標籤: algorithms前端
[TOC]java
本文介紹幾種常見的數據結構:棧、隊列、堆、哈希表,等等。node
本文全部圖片均截圖自coursera上普林斯頓的課程《Algorithms, Part I》中的Slidesgit
相關命題的證實可參考《算法(第4版)》github
源碼可在官網下載,也能夠在個人github倉庫 algorithms-learning下載,已經使用maven構建算法
倉庫下載:git clone git@github.com:brianway/algorithms-learning.git
數組
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; } }
上面的實現會有幾個問題:
從空棧pop會拋出異常
插入元素過多會超出數組上界
這裏重點解決第二個問題,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).
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.
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]; } }
使用數組來表示一個二叉堆。根節點索引從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.
有兩種狀況會觸發節點移動:
子節點的鍵值變爲比父節點大
父節點的鍵值變爲比子節點(一個或兩個)小
而 要消除這種違反最大堆定義的結構,就須要進行節點移動和交換, 使之知足父節點鍵值不小於兩個子節點 。對應的操做分別是 上浮 和 下沉
上浮:子節點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.否則就不能保證父節點不小於子節點。
鍵值對的抽象.其中鍵通常使用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)]
兩種實現的數據結構:
無序鏈表:Maintain an (unordered) linked list of key-value pairs.
有序數組:Maintain an ordered array of key-value pairs.
在有序數組進行查找時使用二分查找。兩種方式的對好比下圖:
上面幾種數據結構都是經過遍歷或者二分查找去搜尋某個元素,而哈希表則是經過一個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
Use an array of M < N linked lists.
哈希:將key映射到0 ~ M-1 之間的一個整數i
插入:將值插在第i個鏈的前端
查找:只需遍歷第i個鏈
開放地址:若是發生衝突,將值放入下一個空的位置.(數組尺寸 M 必須比鍵值對的數目 N 要多.)
哈希:將key映射到 0 ~ M-1 之間的一個整數i
插入:若是數組索引爲 i 的位置爲空,則把值放入,不然依次嘗試 i+1,i+2等索引,直到有空的
查找:先找索引 i,若是被佔用且沒匹配,則依次嘗試i+1, i+2,等等