常見數據結構(一)-棧,隊列,堆,哈希表

常見數據結構(一)-棧,隊列,堆,哈希表

標籤: algorithms前端


[TOC]java


本文介紹幾種常見的數據結構:棧、隊列、堆、哈希表,等等。node

寫在前面

Stacks(棧)

LIFO(後進先出):last in first out.數據結構

  • 使用linked-list實現app

保存指向第一個節點的指針,每次從前面插入/刪除節點。less

以字符串棧爲例,示例代碼:

public class LinkedStackOfStrings {
    private Node first = null;

    private class Node {
        String item;
        Node next;
    }

    public boolean isEmpty() {
        return first == null;
    }

    public void push(String item) {
        Node oldfirst = first;
        first = new Node();
        first.item = item;
        first.next = oldfirst;
    }

    public String pop() {
        String item = first.item;
        first = first.next;
        return item;
    }
}
  • 使用數組實現

使用數組來存儲棧中的項

public class FixedCapacityStackOfStrings {
    private String[] s;
    private int N = 0;

    public FixedCapacityStackOfStrings(int capacity) {
        s = new String[capacity];
    }

    public boolean isEmpty() {
        return N == 0;
    }

    public void push(String item) {
        s[N++] = item;
    }

    public String pop() {
        String item = s[--N];
        s[N] = null;
        return item;
    }
}

上面的實現會有幾個問題:

  1. 從空棧pop會拋出異常

  2. 插入元素過多會超出數組上界

這裏重點解決第二個問題,resizing arrays.一個可行的方案是: 當數組滿的時候,數組大小加倍;當數組是1/4滿的時候,數組大小減半。 這裏不是在數組半滿時削減size,這樣能夠避免數組在將滿未滿的臨界點屢次push-pop-push-pop操做形成大量的數組拷貝操做。

插入N個元素,N + (2 + 4 + 8 + ... + N) ~ 3N

  • N:1 array access per push

  • (2 + 4 + 8 + ... + N):k array accesses to double to size k (ignoring cost to create new array)

因爲resize操做不是常常發生,因此均攤下來,平均每次push/pop操做的仍是常量時間(constant amortized time).

Queues(隊列)

FIFO(先進先出):first in first out.

  • 使用linked-list實現

保存指向首尾節點的指針,每次從鏈表尾插入,從鏈表頭刪除。

public class LinkedQueueOfStrings {
    private Node first, last;

    private class Node {  
        /* same as in StackOfStrings */
    }

    public boolean isEmpty() {
        return first == null;
    }

    public void enqueue(String item) {
        Node oldlast = last;
        last = new Node();
        last.item = item;
        last.next = null;
        if (isEmpty()) {
            first = last;
        } else {
            oldlast.next = last;
        }
    }

    public String dequeue() {
        String item = first.item;
        first = first.next;
        if (isEmpty()) last = null;
        return item;
    }
}
  • 使用數組實現

・Use array q[] to store items in queue.
・enqueue(): add new item at q[tail].
・dequeue(): remove item from q[head].
・Update head and tail modulo the capacity.
・Add resizing array.

Priority Queues

Collections. Insert and delete items.

  • Stack. Remove the item most recently added.

  • Queue. Remove the item least recently added. Randomized queue. Remove a random item.

  • Priority queue. Remove the largest (or smallest) item.

unordered array 實現

public class UnorderedMaxPQ<Key extends Comparable<Key>> {
    private Key[] pq;   // pq[i] = ith element on pq
    private int N;      // number of elements on pq

    public UnorderedMaxPQ(int capacity) {
        pq = (Key[]) new Comparable[capacity];
    }

    public boolean isEmpty() {
        return N == 0;
    }

    public void insert(Key x) {
        pq[N++] = x;
    }

    public Key delMax() {
        int max = 0;
        for (int i = 1; i < N; i++)
            if (less(max, i)) max = i;

        exch(max, N - 1);
        return pq[--N];
    }
}

Binary Heaps(二叉堆)

使用數組來表示一個二叉堆。根節點索引從1開始。索引對應在樹中的位置,最大的鍵值是a[1],同時也是二叉樹的根節點。

  • Parent's key no smaller than children's keys

  • Indices start at 1.

  • Parent of node at k is at k/2.

  • Children of node at k are at 2k and 2k+1.

數組表示二叉堆

上浮和下沉

有兩種狀況會觸發節點移動:

  1. 子節點的鍵值變爲比父節點大

  2. 父節點的鍵值變爲比子節點(一個或兩個)小

要消除這種違反最大堆定義的結構,就須要進行節點移動和交換, 使之知足父節點鍵值不小於兩個子節點 。對應的操做分別是 上浮下沉

  • 上浮:子節點key比父節點大

    • Exchange key in child with key in parent.

    • Repeat until heap order restored.

  • 下沉:父節點key比子節點(one or both)小

    • Exchange key in parent with key in larger child.

    • Repeat until heap order restored

/* 上浮 */
private void swim(int k) {
    while (k > 1 && less(k / 2, k)) {
        exch(k, k / 2);
        k = k / 2;
    }
}

/* 下沉 */
private void sink(int k) {
   while (2 * k <= N) {
       int j = 2 * k;
       if (j < N && less(j, j + 1)) j++;
       if (!less(k, j)) break;
       exch(k, j);
       k = j;

   }
}

插入和刪除

全部操做(插入和刪除)都保證在log N 時間內。

  • 插入:二叉堆的插入操做比較簡單,把節點加在數組尾部,而後上浮便可。

  • 刪除最大:二叉堆的刪除則是把根節點和末尾的節點交換,而後下沉該節點便可。

/* 插入 */
public void insert(Key x){
  pq[++N] = x;
  swim(N);
}

/* 刪除 */
public Key delMax(){
  Key max = pq[1];
  exch(1, N--);
  sink(1);
  pq[N+1]=null;
  return max;
}

最後,堆中的鍵值是不能變的,即Immutable.否則就不能保證父節點不小於子節點。

Symbol Tables

鍵值對的抽象.其中鍵通常使用immutable的類型,值是任何普通類型。

關於比較,全部的java類都繼承了equals()方法,要求對於引用x,y,z

  • Reflexive: x.equals(x) is true.

  • Symmetric: x.equals(y) iff y.equals(x).

  • Transitive: if x.equals(y) and y.equals(z), then x.equals(z).

  • Non-null: x.equals(null) is false.

對於用戶自定義的類型,通常按以下流程實現equals()方法:

  • Optimization for reference equality.

  • Check against null.

  • Check that two objects are of the same type and cast.

  • Compare each significant field:

    • if field is a primitive type, use ==

    • if field is an object, use equals().[apply rule recursively]

    • if field is an array, apply to each entry.[alternatively, use Arrays.equals(a, b) or Arrays.deepEquals(a, b),but not a.equals(b)]

兩種實現的數據結構:

  1. 無序鏈表:Maintain an (unordered) linked list of key-value pairs.

  2. 有序數組:Maintain an ordered array of key-value pairs.

在有序數組進行查找時使用二分查找。兩種方式的對好比下圖:

elementary ST implementations summary

Hash Tables(哈希表)

上面幾種數據結構都是經過遍歷或者二分查找去搜尋某個元素,而哈希表則是經過一個key-indexed table來存儲其中的項,即「索引」是「鍵」的一個函數。換句話說,哈希是經過定義一種函數/計算方法,把鍵直接映射成一個哈希值(再經過取餘操做換算成數組的下標索引),從而定位元素,而避免耗時的逐個比較和遍歷的操做。

  • Hash code:An int between -2^31 and 2^31 - 1.

  • Hash function. An int between 0 and M - 1 (for use as array index).

//這裏hashCode可能爲負,且-2^31取絕對值會溢出,因此要「位與」
private int hash(Key key){
    return (key.hashCode() & 0x7fffffff) % M;  
}

全部的java類均繼承了hashCode()方法來計算哈希值, 返回一個32-bit的int.默認實現是返回該對象的內存地址。對經常使用的類型有本身的實現,以java的String類爲例子:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

hash code design."Standard" recipe for user-defined types:

  • Combine each significant field using the 31x + y rule.

  • If field is a primitive type, use wrapper type hashCode().

  • If field is null, return 0.

  • If field is a reference type, use hashCode().[applies rule recursively]

  • If field is an array, apply to each entry.[or use Arrays.deepHashCode()]

固然,這種映射並不能保證是一對一的,因此必定會出現多個鍵映射到同一個哈希值的尷尬狀況(尤爲是對數組的size取餘操做後,映射到同一數組下標),即哈希衝突,這是就須要一些方法來解決。這裏介紹兩種經常使用的方法:

  • separate chaining

  • linear probing

separate chaining

Use an array of M < N linked lists.

  • 哈希:將key映射到0 ~ M-1 之間的一個整數i

  • 插入:將值插在第i個鏈的前端

  • 查找:只需遍歷第i個鏈

separate chaining

linear probing

開放地址:若是發生衝突,將值放入下一個空的位置.(數組尺寸 M 必須比鍵值對的數目 N 要多.)

  • 哈希:將key映射到 0 ~ M-1 之間的一個整數i

  • 插入:若是數組索引爲 i 的位置爲空,則把值放入,不然依次嘗試 i+1,i+2等索引,直到有空的

  • 查找:先找索引 i,若是被佔用且沒匹配,則依次嘗試i+1, i+2,等等


做者@brianway更多文章:我的網站 | CSDN | oschina

相關文章
相關標籤/搜索