Study Notes of Data Structures

Arrays

A static array is a fixed length container containing n elements ​ from the range [0, n-1]. indexable means that each slot/index in the array can be referenced with a number.

When and where is a static array used?

  • Storing and accessing sequential data

  • Temporarily storing objects

  • Used by IO routines as buffers

  • Lookup tables and inverse lookup tables

  • Can be used to return multiple values

  • Used in dynamic programming to cache answers to subproblems

Complexity

Static Array Dynamic Array
Access O(1) O(1)
Search O(n) O(n)
Insertion N/A O(n)
Appending N/A O(1)
Deletion N/A O(n)

Elements in an Array are referenced by their index. There is no other way to access elements in an array. Array indexing is zero-based, meaning the first element is found in position zero.

Operations on Dynamic Array

The dynamic array can grow and shrink in size.

How can we implement a dynamic array?

One way is to use a static array!

1) Create a static array with an initial capacity.

2) Add elements to the underlying static array, keeping track of the number of elements.

3) If adding another element will exceed the capacity, then create a new static array with twice the capacity and copy the original elements into it.

Implementation of an dynamic array

@SuppressWarnings("unchecked")
public class Array<T> implements Iterable<T> {
    
    private T[] arr;
    private int len = 0;//length user thinks array is
    private int capacity = 0;//actual array size
    
    public Array() { this(16); }
    
    public Array(int capacity) {
        if (capacity < 0) throw new IllegalArgumentException("Illegal Capacity:  " + capacity);
        this.capacity = capacity;
        arr = (T[]) new Object[capacity];
    }
    
    public int size() { return len; }
    public boolean isEmpty() { return size() == 0; }
    
    public T get(int index) { return arr[index]; }
    public void set(int index, T elem) { arr[index] = elem; }
    
    public void clear() {
        for (int i = 0; i < capacity; i++)
            arr[i] = null;
        len = 0;
    }
    
    public void add(T elem) {
        //Time to resize!
        if (len + 1 >= capacity) {
            if (capacity == 0) capacity = 1;
            else capacity *= 2; //double the size
            T[] new_arr = (T[]) new Object[capacity];
            for (int i = 0; i < len; i++) 
                new_arr[i] = arr[i]; //copy values from old array to new array
            arr = new_arr; //arr has extra nulls padded
        }
        arr[len++] = elem;
    }
    
    //Removes the element at the specified index in this list.
    public T removeAt(int rm_index) {
        if (rm_index >= len && rm_index < 0) throw new IndexOutOfBoundsException();
        T data = arr[rm_index];
        T[] new_arr = (T[]) new Object[len - 1];
        for (int i = 0, j = 0; i < len; i++, j++) {
            if (i == rm_index) j--; //Skip over rm_index by fixing j temporarily
            else new_arr[j] = arr[i];
        }
        arr = new_arr;
        capacity = --len;
        return data;
    }
    
    pubic boolean remove(Object obj) {
        for (int i = 0; i < len; i++) {
            if (arr[i].equals(obj)) {
                removeAt(i);
                return true;
            }
        }
        return false;
    }
    
    public int indexOf(Object obj) {
        for (int i = 0; i < len; i++)
            if (arr[i].equals(obj))
                return i;
        return -1;
    }
    
    public boolean contains(Object obj) {
        return indexOf(obj) != -1;
    }
    
    //Iterator is still fast but not as fast as iterative for loop
    @Override
    public java.util.Iterator <T> iterator() {
        return new java.util.Iterator<T>() {
            int index = 0;
            public boolean hasNext() { return index < len; }
            public T next() { return arr[index++]; }
        };
    }
    
    @Override
    public String toString() {
        if (len == 0) return "[]";
        else {
            StringBuilder sb = new StringBuilder(len).append("[]");
            for (int i = 0; i < len - 1; i++)
                sb.append(arr[i] + ", ");
            return sb.append(arr[len - 1] + "]").toString();
        }
    }
}

Singly and Doubly Linked Lists

What is a linked list?

A linked list is a sequential list of nodes that hold data which point to other nodes also containing data.

Data -> Data -> Data -> Data -> null

Where are linked lists used?

  • Used in many List, Queue & Stack implementations.

  • Great for creating circular lists.

  • Can easily model real world objects such as trains.

  • Used in separate chaining, which is present certain HashTable implementations to deal with hashing collisions.

  • Often used in the implementation of adjacency lists for graphs.

Terminology

Head: The first node in a linked list

Tail: The last node in a linked list

Pointer: Reference to another node

Node: An object containing data and pointer(s)

Singly vs Doubly Linked Lists

Singly linked lists only hold a reference to the next node. In the implementation you always maintain a reference to the ​ to the linked list and a reference to the ​ node for quick additions/removals.

With a doubly linked list each node holds a reference to the next and previous node. In the implementation you always maintain a reference to the head and the tail of the doubly linked list to do quick additions/removals from both ends of your list.

Pros Cons
Singly Linked Uses less memory . Simpler implementation. Cannot easily access previous elements
Doubly Linked Can be traversed backwards Takes 2 times memory

Complexity Analysis

Singly Linked Doubly Linked
Search O(n) O(n)
Insert at head O(1) O(1)
Insert at tail O(1) O(1)
Remove at head O(1) O(1)
Remove at tail O(n) O(1)
Remove in middle O(n) O(n)

Implementation of a Doubly Linked List

public class DoublyLinkedList<T> implements Iterable<T> {
    
    private int size = 0;
    private Node<T> head = null;
    private Node<T> tail = null;
    
    //Internal node class to represent data
    private class Node<T> {
        T data;
        Node<T> prev;
        Node<T> next;
        public Node(T data, Node<T> prev, Node<T> next) {
            this.data = data;
            this.prev = prev;
            this.next = next;
        }
        @Override
        public String toString() {
            return data.toString();
        }
    }
    
    //Empty this linked list, O(n)
    public void clear() {
        Node<T> trav = head;
        while (trav != null) {
            Node<T> next = trav.next; //tracking the next node
            trav.prev = trav.next = null; //remove two pointers of node trav
            trav.data = null; //remove data of node trav
            trav = next; // next node to be removed
        }
        head = tail = trav = null;
        size = 0;
    }
    
    //Return the size of this linked list
    public int size() {
        return size;
    }
    
    //Is this linked list empty?
    public boolean isEmpty() {
        return size() == 0;
    }
    
    //Add an element to the tail of the linked list, O(1)
    public void add(T elem) {
        addLast(elem);
    }
    
    //Add an element to the beginning of this linked list, O(1)
    public void addFirst(T elem) {
        //The linked list is empty
        if (isEmpty()) {
            head = tail = new Node<T>(elem, null, null);
        } else {
            head.prev = new Node<T>(elem, null, head/* set the next pointer of this new node points to the current head node */);
            head = head.prev; //reset the head node
        }
        size++;
    }
    
    //Add a node to the tail of the linked list, O(1)
    public void addLast(T elem) {
        //The linked list is empty
        if (isEmpty()) {
            head = tail = new Node<T>(elem, null, null);
        } else {
            tail.next = new Node<T>(elem, tail, null);
            tail = tail.next;
        }
        size++;
    }
    
    //Check the value of the first node if it exists, O(1)
    public T peekFirst() {
        if (isEmpty()) throw new RuntimeException("Empty list");
        return head.data;
    }
    
    //Check the value of the last node if it exists, O(1)
    public T peekLast() {
        if (isEmpty()) throw new RuntimeException("Empty list");
        return tail.data;
    }
    
    //Remove the first value at the head of the linked list, O(1)
    public T removeFirst() {
        //Can't remove data from an empty list -_-
        if (isEmpty()) throw new RuntimeException("Empty list");
        
        //Extract the data at the head and move
        //the head pointer forwards one node
        T data = head.data;
        head = head.next;
        --size;
        
        //If the list is empty set the tail to null as well
        if (isEmpty()) tail = null;
        //Do a memory clean of the previous node
        else head.prev = null;
        
        //Return the data that was at the first node we just removed
        return data;
    }
    
    //Removed the last value at the tail of the linked list, O(1)
    public T removeLast() {
        //Can't remove data from an empty list -_-
        if (isEmpty()) throw new RuntimeException("Empty list");
        
        //Extract the data at the tail and move
        //the tail pointer backwards one node
        T data = tail.data;
        tail = tail.prev;
        --size;
        
        //If the list is now empty set the head to null as well
        if (isEmpty()) head = null;
        //Do a memory clean of the node that was just removed
        else tail.next = null;
        
        //Return the data that was at the first node we just removed
        return data;
    }
    
    //Removed an arbitrary node from the linked list, O(1)
    private T remove(Node<T> node) {/* the Node is private, so don't let user access it */
        //If the node to remove is somewhere either at the
        //head or the tail handle those independently
        if (node.prev == null) return removeFirst();
        if (node.next == null) return removeLast();
        
        //Make the pointers of adjacent nodes skip over 'node'
        node.next.prev = node.prev;
        node.prev.next = node.next;
        
        //Temporary store the data we want to return
        T data = node.data;
        
        //Memory cleanup
        node.data = null;
        node = node.prev = node.next = null;
        --size;
        
        //Return the data at the node we just removed
        return data;
    }
    
    //Remove a node at a particular index, O(n)
    public T removeAt(int index) {
        //Make sure the index provided is valid -_-
        if (index < 0 || index >= size) throw new IllegalArgumentException();
        
        int i;
        Node<T> trav;
        
        //Search from the front of the list
        if (index < size/2) {
            for (i = 0, trav = head; i != index; i++)
                trav = trac.next
        } else {
            for (i = size - 1, trav = tail; i != index; i--)
                trav = trav.prev;
        }
        return remove(trav);
    }
    
    //Remove a particular value in the linked list, O(n)
    public boolean remove(Object obj) {
        Node<T> trav = head;
        
        //Support searching for null
        if (obj == null) {
            for (trav = head; trav != null; trav = trav.next) {
                if (trav.data == null) {
                    remove(trav);
                    return true;
                }
            }
        } else { /* search for non null object */
            for (trav = head; trav != null; trav = trav.next) {
                if (obj.equals(trav.data)) {
                    remove(trav);
                    return true;
                }
            }
        }
        return false;
    }
    
    //Find the index of a particular value in the linked list, O(n)
    public int indexOf(Object obj) {
        int index = 0;
        Node<T> trav = head;
        
        //Support searching for null
        if (obj == null) {
            for (trav = head; trav != null; trav = trav.next, index++)
                if (trav.data == null)
                    return index;
        } else { /* search for non null object */
            for (trav = head; trav != null; trav = trav.next, index++)
                if (obj.equals(trav.data))
                    return index;
        }
        return -1;
    }
    
    //Check is a value is contained within the linked list
    public boolean contains(Object obj) {
        return indexOf(obj) != -1;
    }
    
    @Override 
    public java.util.Iterator <T> iterator() {
        return new java.util.Iterator<T>() {
            private Node<T> trav = head;
            @Override
            public boolean hasNext() {
                return trav != null;
            }
            @Override
            public T next() {
                T data = trav.data;
                trav = trav.next;
                return data;
            }
        };
    }
    
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[ ");
        Node<T> trav = head;
        while (trav != null) {
            sb.append(trav.data + ", ");
            trav = trav.next;
        }
        sb.append(" ]");
        return sb.toString();
    }
}

Stack

What is a Stack?

A stack is a one-ended linear data structure which models a real world stack by having two primary operations, namely ​ and ​.

When and where is a Stack used?

  • Used by undo mechanisms in text editors.

  • Used in compiler syntax checking for matching brackets and braces.

  • Can be used to model a pile of books or plates.

  • Used behind the scenes to support recursion by keeping track of previous function calls.

  • Can be used to do a Depth First Search (DFS) on a graph.

Complexity

Pushing O(1)
Popping O(1)
Peeking O(1)
Searching O(n)
Size O(1)

Example of Stack's usage - Brackets matching

Let S be a stack
For bracket in bracket_string:
    reversed = getReversedBracket(bracket) //get the reversed bracket of the 'bracket'
    If isLeftBracket(bracket):
        S.push(bracket)
    Else If S.isEmpty() or S.pop() != reversed:
        return false //Invalid
return S.isEmpty() //Valid if S is empty

Implementation of a Stack

public class Stack<T> implements Iterable<T> {
    
    private java.util.LinkedList<T> list = new java.util.LinkedList<T>();
    
    //Create an empty stack
    public Stack() {}
    
    //Create a Stack with an initial element
    public Stack(T firstElem) {
        push(firstElem);
    }
    
    //Return the number of elements in the stack
    public int size() {
        return list.size();
    }
    
    //Check if the stack is empty
    public boolean isEmpty() {
        return size() == 0;
    }
    
    //Push an element on the stack
    public void push(T elem) {
        list.addLast(elem);
    }
    
    //Pop an element off the stack
    //Throws an error if the stack is empty
    public T pop() {
        if (isEmpty())
            throw new java.util.EmptyStackException();
        return list.removeLast();
    }
    
    //Peek the top of the stack without removing an element
    //Throws an exception if the stack is empty
    public T peek() {
        if (isEmpty())
            throw new java.util.EmptyStackException();
        return list.peekLast();
    }
    
    //Allow users to iterate through the stack using an iterator
    @Override
    public java.util.Iterator<T> iterator() {
        return list.iterator();
    }
}

Queues

What is a queue?

A queue is linear data structure which models real world queues by having two primary operations, namely ​ and ​.

Queue Terminology

There does not seem to be consistent terminology for inserting and removing elements from queues.

Enqueue = Adding = Offering

Dequeue = Polling

When and where is a Queue used?

  • Any waiting line models a queue, for example a lineup at a movie theater.

  • Can be used to efficiently keep track of the X most recently added elements.

  • Web server request management where you want first come first serve.

  • Breadth first search (BFS) graph traversal.

Complexity

Enqueue O(1)
Dequeue O(1)
Peeking O(1)
Contains O(n)
Removal O(n)
Is Empty O(1)

Queue Example - BFS

Let Q be a Queue
Q.enqueue(starting_node)
startint_node.visited = true
​
while Q is not empty Do
    node = Q.dequeue()
    For neighbour in neighbours(node):
        If neighbour has not been visited:
            neighbour.visited = true
            Q.enqueue(neighbour) //for visiting next layer neighbours

Queue Implementation Details

public class Queue<T> implements Iterable<T> {
    
    private javal.util.LinkedList<T> list = new java.util.LinkedList<T>();
    
    public Queue() {}
    
    public Queue(T firstElem) {
        offer(firstElem);
    }
    
    //Return the size of the queue
    public int size() {
        return list.size();
    }
    
    //Returns whether or not the queue is empty
    public boolean isEmpty() {
        return size() == 0;
    }
    
    //Peek the element at the front of the queue
    //The method throws an error if the queue is empty
    public T peek() {
        if (isEmpty())
            throw new RuntimeException("Queue Empty");
        return list.peekFirst();
    }
    
    //Poll an element from the front of the queue
    //The method throws an error if the queue is empty
    public T poll() {
        if (isEmpty())
            throw new RuntimeException("Queue Empty");
        return list.removeFirst();
    }
    
    //Add an element to the back of the queue
    public void offer(T elem) {
        list.addLast(elem);
    }
    
    //Return an iterator to allow the user to traverse
    //through the elements found inside the queue
    @Override
    public java.util.Iterator<T> iterator() {
        return list.iterator();
    }
}

Priority Queue

What is a Priority Queue?

A priority queue is an abstract data type that operates similar to a normal queue except that ​ ​ ​ ​ ​ ​. The priority of the elements in the priority queue determine the order in which elements are removed from the PQ.

NOTE: Priority queues only supports comparable data, meaning the data inserted into the priority queue must be able to be ordered in some way either from least to greatest or greatest to least. This is so that we are able to assign relative priorities to each element.

What is a Heap?

A heap is a ​ based DS that satisfies the heap invariant (also called heap property): If A is a parent node of B then A is ordered with respect to B for all nodes A, B in the heap.

When and where is aPQ used?

  • Used in certain implementations of Dijkstra's Shortest Path algorithm.

  • Anytime you need the dynamically fetch the 'next best' or 'next worst' element.

  • Used in Huffman coding (which is often used for lossless data compression).

  • Best First Search (BFS) algorithms such as A* use PQs to continuously grab the next most promising node.

  • Used by Minimum Spanning Tree (MST) algorithms.

Complexity PQ with binary heap

Binary Heap construction O(n)
Polling O(log(n))
Peeking O(1)
Adding O(log(n))
Naive Removing O(n)
Advanced removing with help from a hash table * O(log(n))
Naive contains O(n)
Contains check with help of a hash table * O(1)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值