算法爬坑记—— 栈,队列,哈希表,堆

常考数据结构:

高频: 二叉树,队列,链表, 数组,hashmap

中:堆,并查集, Tire

少:树状数组,栈,双端队列,单调栈


队列queue

队列的作用是实现一个先进先出的数据结构。Java中创建队列的方法:

Queue<Integer> queue = new LinkedList<>()
Queue<Integer> queue = new ArrayDequeue<>()

两者的区别在于,Array对于随机访问有较好的性能;List对于插入和删除有较好的性能。

队列的主要操作有:

add()/offer():作用都是向队列中添加元素

 poll():返回队列中第一个元素,并队列中的元素

 size():

 isEmpty():

使用链表实现一个队列类。采用了 dummy node

class QueueNode {
    public int val;
    public QueueNode next;
    public QueueNode(int value) {
        val = value;
    }
}

public class Queue {

    private QueueNode dummy, tail;

    public Queue() {
        dummy = new QueueNode(-1);
        tail = dummy;
    }

    public void enqueue(int val) {
        QueueNode node = new QueueNode(val);
        tail.next = node;
        tail = node;  // 每次添加节点之后需要更新这个 tail
    }

    public int dequeue() {
        int ele = dummy.next.val;
        dummy.next = dummy.next.next;

        if (dummy.next == null) {
            tail = dummy;    // reset
        }
        return ele;
    }

    public int peek() {
        int ele = dummy.next.val;
        return ele;
    }

    public boolean isEmpty() {
        return dummy.next == null;
    }
}

使用两个stack实现queue

public class MyQueue {
    private Stack<Integer> stack1;
    private Stack<Integer> stack2;
    
    public MyQueue() {
        stack1 = new Stack<Integer>();
        stack2 = new Stack<Integer>();
    }
    
    private void stack2ToStack1() {
        while (! stack2.empty()) {
            stack1.push(stack2.peek());
            stack2.pop();
        }
    }
	
    public void push(int number) {
        stack2.push(number);
    }

    public int pop() {
        if (stack1.empty() == true) {
            this.stack2ToStack1();
        }
        return stack1.pop();
    }

    public int top() {
        if (stack1.empty() == true) {
            this.stack2ToStack1();
        }
        return stack1.peek();
    }
}

Moving average from data stream:有的数进去,有的数出来。典型队列问题。

使用循环数组实现队列

public class CircularQueue {
    
    int[] circularArray;
    int front;
    int rear;
    int size;
    public CircularQueue(int n) {
        // initialize your data structure here
        
        this.circularArray = new int[n];
        front = 0;
        rear = 0;
        size = 0;
    }
    /**
     * @return:  return true if the array is full
     */
    public boolean isFull() {
        // write your code here 
        return size == circularArray.length;
    }

    /**
     * @return: return true if there is no element in the array
     */
    public boolean isEmpty() {
        // write your code here
        return size == 0;
    }

    /**
     * @param element: the element given to be added
     * @return: nothing
     */
    public void enqueue(int element) {
        // write your code here
        if (isFull()) {
            throw new RuntimeException("Queue is already full");
        }
        rear = (front + size) % circularArray.length;
        circularArray[rear] = element;
        size += 1;
    }

    /**
     * @return: pop an element from the queue
     */
    public int dequeue() {
        // write your code here
        if (isEmpty()) {
            throw new RuntimeException("Queue is already empty");
        }
        int ele = circularArray[front];
        front = (front + 1) % circularArray.length;
        size -= 1;
        return ele;
    }
}

栈stack

栈的主要操作:

pop() :返回栈顶元素,但删除这个栈顶元素。

push():压栈

empty():判断栈是否为空

peek():返回栈顶元素,但不删除栈顶元素。

实现非递归的主要数据结构。二叉树的迭代器。DFS做递归的时候就是操作系统帮你实现的栈。数据结构的时间复杂度:要指明这个数据结构通过哪一中算法方法实现所占用的时间复杂度是多少。一个数据结构中的方法可以采用不同的算法实现,比如lowerBound 和add可以通过遍历或者红黑树实现。如果add调用比较多,遍历比较好;如果lowerBound调用比较多,则红黑树比较好。


哈希表HashMap

HashMap的操作:

get()

put()

操作时间复杂度是:O(size of key) 

所有操作的时间复杂度是O(size of key),因为hash function是针对key进行操作的。 如果存的key是 Integer , reference内存地址(直接把object(TreeNode之类的)当作key放到里面。其实是存的内存地址),这些的key size 可以是1。但是字符串是不能认为长度是1。如果key是object,HashFunction以这个object的内存地址作为key进行处理。

HashMap的原理:利用 hash function 将key变成一个数组的下标。但是可能会造成 collision。当产生 collision 的时候,可以有两种解决办法。

open hashing:数组中每个位置存的是Linkedlist中的dummy node。如果产生collison,就在这个linkedlist中存储的dummy node后面加上新进入的数。

close hash:只能存储有限多个数据。如果产生collision,则占用下一个位置。在find的过程中,先找到应该对应的位置,如果这个位置上有别的元素,则向后找,直到再次找到或者遇到空位置。在delete过程中,如果delete一个数,则需要在这个位置上表明delete,否则无法找到目标数。

Rehashing:使用倍增的思路。但是在扩容之后需要把之前的数重新hash一遍。所以rehashing 的过程非常慢。

一般是用open hashing。open hash的实现:

hash function 的目标是得到一个  稳定且无规律  的下标。

实现方式:边取模边除,从而防止结果溢出

public class Solution {
    public int hashCode(char[] key, int HASH_SIZE) {
        // write your code here
        long ans = 0;
        for (int i = 0; i < key.length; i++) {
            ans = (ans * 33 + (int) (key[i])) % HASH_SIZE;
        }
        
        return (int)ans;
    }
}

LRU Cache(Least Recently Used):

这种算法是计算淘汰cache里面哪些数据。把最近没用过的数据扔掉。如果一个数据被访问了,就把这个数据放到最后的位置,这样做的话,最近没用的数据一直在第一位。

LRU需要支持append,pop,delete。可以直接用linkedlist来实现这些操作。为了在O(1)的时间内找到某个点,用hashmap来存链表的节点。key:cache key,value:linkedlist ListNode。为了得到这个节点前面的那个节点,可以将LinkedList 设置为 doublly list或者在hashmap中直接存这个点前面的节点。

Insert Delete GetRandom/ load balanced:add/remove/pick的操作都要是在O(1)的时间内完成。remove操作可能破坏了数组的连续性。所以如果中间某个位置被remove,将数组最后一个元素补到那个位置去。

First Unique Character in a String/First Unique Character StringII:难点在于data stream,不能从头遍历这个data stream。所以使用一个序列来存储只出现一次的数字。所以和LRC一样,用一个hashmap和linkedlist来做。

堆Heap

Heap分为 min heap 和 max heap

堆是一个二叉树。 堆的结构特性是从上到下从小到大添加元素。进来一个元素之后要先进其预订的位置,如果这个预订的位置不符合堆的条件,则向上交换。这样就保证整个二叉树的高度是logN。堆的数据特性分为最小堆和最大堆。最小堆的父节点值要比儿子节点小。最大堆的父亲节点要比儿子节点大。左右儿子节点之间没什么关系。

注意与BST的区别。

堆能进行的操作: 

 add():O(log(n))

poll(): O(log(n))

peak(): O(1)

Sift Up:先放在叶子节点,然后向上交换。每次插入之后,最多调整 log(N)次。

Sift Down:删除根节点,然后把最后一个叶子节点放到根节点。然后跟两个儿子中的值最小的进行交换。最多调整log(N)次。

Priority Queue 中如果想要删除任意的节点,是先把整个堆遍历一边,时间复杂度是O(N)。

如果heap想要快速删除某个节点:需要有一个map进行对应,以快速找到某一个节点的位置。key是节点的值,value是值的index。

堆的实现:由于堆的结构是固定的。可以通过普通数组来实现一个堆。通过数组下标即可得到堆的位置。

数组第0位:存储堆中有多少个元素

求父亲节点:n/2

左儿子:n * 2

右儿子:n * 2 + 1

将一个数组变成一个堆:

Sift up

public class Solution {
    /**
     * @param A: Given an integer array
     * @return: void
     */
    private void siftup(int[] A, int k) {
        while (k != 0) {
            int father = (k - 1) / 2;
            if (A[k] > A[father]) {
                break;
            }
            int temp = A[k];
            A[k] = A[father];
            A[father] = temp;
            
            k = father;
        }
    }
    
    public void heapify(int[] A) {
        for (int i = 0; i < A.length; i++) {
            siftup(A, i);
        }
    }
}

Sift down

public class Solution {
    /**
     * @param A: Given an integer array
     * @return: void
     */
    private void siftdown(int[] A, int k) {
        while (k * 2 + 1 < A.length) {
            int son = k * 2 + 1;   // A[i] 的左儿子下标。
            if (k * 2 + 2 < A.length && A[son] > A[k * 2 + 2])
                son = k * 2 + 2;     // 选择两个儿子中较小的。
            if (A[son] >= A[k])      
                break;
            
            int temp = A[son];
            A[son] = A[k];
            A[k] = temp;
            k = son;
        }
    }
    
    public void heapify(int[] A) {
        for (int i = (A.length - 1) / 2; i >= 0; i--) {
            siftdown(A, i);
        }
    }
}

K路归并:有多种实现方法。

1. 比较K个链表的链表头。使用priority queue在K个表头中寻找一个最小值。时间复杂度O(N * logK) N是所有节点的个数 。

Priority Queue,名字叫队列,但是是根据heap来实现的。可以认为priority queue是一个阉割版的heap。

2. 两两归并。最终时间复杂度也是O(N *logK)

3. 归并排序法。实际上是分治的思想。跟两两归并其实是差不多的。两两归并是从底向上,而归并排序是从上到底。和彩虹排序代码一样。

Ugly number:用priority queue,每次找出一个最小的数出来乘2 乘3 乘5,把得到的数放回priority queue中。用一个hashset保证不会加入重复的数。java中没有heap,只有priority queue。Treeset。

Merge K Sorted Lists:归并有序序列。将最小的取出之后,把其后面的点顶到前面去。可用priority queue实现。时间复杂度是O(N*logK)K是priority queue中元素个数。N是所有元素个数。

K个最近的点:用一个priority queue 一直保持着k个最近的点。时间复杂度是O(N*logK)。可以用 heapify时间复杂度是O(N + K*logN)/quick-select时间复杂度是O(N + N + K*logK)如果排序的话,就加K*logK。如果不排序就拉倒了。

K个最近的点(在线版本):仍然使用priority queue。但是保持的是一个min heap。只需超过priority queue中最小的那个就可以了。

Top K frequent Element:






猜你喜欢

转载自blog.csdn.net/ChichiPly/article/details/80638136