IN1910 – Programming with Scientific Applications Algorithm Analysis and Data Structures
Total Page:16
File Type:pdf, Size:1020Kb
Algorithm analysis Dynamic arrays Linked lists IN1910 – Programming with Scientific Applications Algorithm analysis and data structures Kristian Gregorius Hustad 1,2 1Simula Research Laboratory 2Department of Informatics, University of Oslo Autumn 2020 Algorithm analysis Dynamic arrays Linked lists Plan for the next lectures Tuesday 22.9: Memory, pointers and arrays Thursday 24.9: Classes and object-oriented programming in C++ Tuesday 29.9: Algorithm analysis and dynamic arrays Thursday 1.10: Linked lists Tuesday 6.10 and Thursday 8.10: No lectures Algorithm analysis Dynamic arrays Linked lists Outline 1 Algorithm analysis 2 Dynamic arrays 3 Linked lists Algorithm analysis Dynamic arrays Linked lists Why perform an algorithm analysis? Algorithm analysis can help us understand how the cost of an algorithm scales with the size of the input. The cost could be the computing time or the memory usage of the algorithm. In this course, we are primarily concerned with how the computing time scales with the input size. This is also called the time complexity of the algorithm. Understanding the time complexity of an algorithm, allows us to make predictions about how long a program will take to complete. This is especially important when tackling large problems that may require hours, days or weeks of computing time. Algorithm analysis Dynamic arrays Linked lists Big-Oh notation To describe the time complexity of an algorithm, we use the so-called Big-Oh notation. Definition (Big-Oh upper bound) T (N) = O (f (N)) if there are positive constants c and n0 such that T (N) ≤ cf (N) when N ≥ n0. We can determine the relative growth rates of two functions f (N) and g(N) by f (N) computing limN!1 g(N) . N2 + 2N = O N2 (1) N + log N = O (N) (2) 2N + N2 = O 2N (3) Algorithm analysis Dynamic arrays Linked lists Example 1 – Max of a sorted array Assume we have a sorted array a and we want to find the maximum value. Then we simply return the last element of the array. double sorted_array_max(double*a, int N) { return a[N-1]; } This function requires a constant number of operations; it is constant in time or O (1). The C++ code below shows the different steps that are all carried out in a single line in the function above. double sorted_array_max(double*a, int N) { double*p= a; // copy the address in `a` to a local pointer variable p p +=N-1; // move the pointer N-1 positions up, to point at the last element double val=*p; // read the value at the memory address pointed to by p return val; // return the value } Algorithm analysis Dynamic arrays Linked lists Example 2 – Arithmetic mean of an array Consider the following function that computes the mean of an array. double array_mean(double*a, intN){ double mean=0; for(inti=0; i< N; i++) { mean += a[i]/N; } return mean; } The number of rounds in the for loop is N, and the number of operations inside the body of the for loop is constant, so the total number of operations is a function of N; the function is linear in time or O (N). Algorithm analysis Dynamic arrays Linked lists Example 3 – Adding two N × N matrices Consider the following function that adds two N × N matrices. void matrix_add(double**A, double**B, double**C, intN){ // C = A + B for(inti=0; i< N; i++) { for(intj=0; j< N; j++){ C[i][j]= A[i][j]+ B[i][j]; } } } There are two for loops here, one nested inside the other. The body of the inner for loop will be executed N2 times, so the time complexity of the function is O N2.1 1Of course, one could argue that it’s more natural to choose M = N2 (since the matrices contain N2 entries) and say that the function is linear in time with M or O (M). Algorithm analysis Dynamic arrays Linked lists Example 4 – Multiplying two N × N matrices Consider the following function that multiplies two N × N matrices. void matrix_multiply(double**A, double**B, double**C, intN){ // C = A * B for(inti=0; i< N; i++) { for(intj=0; j< N; j++){ C[i][j]=0; for(intk=0; k< N; k++){ C[i][j] += A[i][k]+ B[k][j]; } } } } This algorithm is O N3. As stated in the lecture notes, there are matrix multiplication algorithms with better time complexity than the standard algorithm given above. The current best one is O N2:3728639 and was published in 2014. But regardless of future algorithmic advances, the time complexity must be at least O N2 since we will always have to read all N2 elements from A and B. Algorithm analysis Dynamic arrays Linked lists The ArrayList class class ArrayList{ private: int*data; int capacity; public: int size; }; Algorithm analysis Dynamic arrays Linked lists ArrayList – constructor and destructor ArrayList() { size=0; capacity= 10000; data= new int[capacity]; } ~ArrayList() { delete[] data; } Algorithm analysis Dynamic arrays Linked lists ArrayList – appending an element If the list size is smaller than the capacity, we can append a new element. But if the list is full, we throw an exception. void append(int n) { if (size< capacity) { data[size]= n; size +=1; } else{ throw std::range_error("Capacity full"); } } Algorithm analysis Dynamic arrays Linked lists ArrayList – getting an element int get(int i) { if((0 <= i) and (i< size)) { return data[i]; } else{ throw std::out_of_range("Index is out of range"); } } Algorithm analysis Dynamic arrays Linked lists ArrayList – testing append and get As we can both add elements and retrieve them, it’s time to test our class. void test1() { ArrayList example; example.append(1); example.append(4); example.append(9); for(inti=0; i<3; i++){ std::cout << "a[" <<i << "] = " << example.get(i) << std::endl; } example.get(3); } Algorithm analysis Dynamic arrays Linked lists ArrayList – printing the list void print() { // prints on the format "[1, 2, 3]" std::cout << "["; if (size>0){ std::cout << data[0]; } for(inti=1; i< size; i++){ std::cout <<"," << data[i]; } std::cout << "]" << std::endl; } Algorithm analysis Dynamic arrays Linked lists ArrayList – initialising from a vector ArrayList(std::vector<int> initial) { size=0; capacity= initial.size(); data= new int[capacity]; for(inte: initial) { append(e); } } We can now initialise the ArrayList like this: void test2() { ArrayList example({0,5, 10, 15}); example.print(); } Algorithm analysis Dynamic arrays Linked lists ArrayList – indexing C++ allows us to overload the indexing operator [], so that we may index our list like we would do for an array. int& operator[] (int i) { if(0 <= i and i< size) { return data[i]; } else{ throw std::out_of_range("IndexError"); } } Note the return type, int&. We return a reference to the array element! This allows the calling code to modify the value of that element, which is necessary if we want to index on the left side of an assigment. ArrayListl({1,2,3}); int val= l[0]; // could be done with int operator[] method l[0] +=1; // needs int& operator[] method Algorithm analysis Dynamic arrays Linked lists ArrayList – growing the list capacity dynamically void grow_capacity() { capacity *= 10; int*tmp= new int[capacity]; for(inti=0; i<size; i++){ tmp[i]= data[i]; } delete[] data; data= tmp; } We can now rewrite append: void append(int n) { if (size >= capacity) { grow_capacity(); } data[size]= n; size +=1; } Algorithm analysis Dynamic arrays Linked lists ArrayList – summary We have created a class, ArrayList, that provides an application programming interface (API) where we can append elements we can retrieve and update existing elements we don’t have to worry about the list’s capacity. It grows automatically when the underlying array is full. Users of ArrayList do not need to know that the underlying data structure is an array. From the outside it just looks like a list. A linked list is a different type of data structure, which also can be used to implement a list. Algorithm analysis Dynamic arrays Linked lists Data structures and abstract data types (ADTs) Algorithm analysis Dynamic arrays Linked lists Node The building block of a linked list is the node. It holds a piece of data and a pointer to the next element in the list. There is one node for each element in the linked list. struct Node{ int value; Node*next; }; Algorithm analysis Dynamic arrays Linked lists Chaining nodes into a linked list Figure: A chain of nodes forming a linked list containing [47, 3, 12, 99, 23]. Algorithm analysis Dynamic arrays Linked lists Simplified drawings of linked lists To simplify our drawings of linked list, we change to drawing our nodes as simple circles, writing their value inside, and the pointer as an arrow. Figure: A linked list containing [5, 31, 17, 90, 51, 1]. Algorithm analysis Dynamic arrays Linked lists A Node class class Node{ public: int value; Node*next; Node(int n) { value= n; next= nullptr; } Node(int n, Node*p) { value= n; next= p; } }; Algorithm analysis Dynamic arrays Linked lists Chaining nodes void test_node_chain() { Node a(12); Node b(57); Node c(36); a.next=&b; b.next=&c; // print node chain std::cout << "["; inti=0; for (Node*n=&a; n != nullptr; n=n->next, i++){ if (i !=0){ std::cout <<","; } std::cout <<n->value; } std::cout << "]" << std::endl; } Algorithm analysis Dynamic arrays Linked lists Appending to the list void append(int val) { if (head == nullptr){ head= new Node(val); return; } // Iterate to end of list Node*current; current= head; while (current->next != nullptr){ current= current->next; } // Link new node to end of list current->next= new Node(val); } Algorithm analysis Dynamic arrays Linked lists Printing the list void print() { if (head == nullptr){ std::cout << "[]" << std::endl; return; } Node* current= head; std::cout << "["; while (current->next != nullptr){