資料結構
本文準備講一下軟體開發中的資料結構。
物理儲存
因為資料結構是用來儲存資料的具體方式,在將資料結構之前,說說資料物理儲存。
平時軟體開發中,一個8G的記憶體可以同一時間儲存8G的資料,在物理上來說,這些儲存單元是連續的,理論上可以可以看成是地址從0開始到 8*2^30次方。理想的話,什麼資料通過定址就能找到,很方便
但是資料的使用在計算機中並不只是查詢,也可以是修改,新增,刪除,移動,考慮的資料安全,運算速度,使用效率的等綜合,基於物理硬體的軟體資料結構往往並不只有陣列,還有其他,比如連結串列,圖,樹等。
在複雜的情況下,各種各樣的運算,不同程式的程式,執行緒都在使用這些記憶體或者cpu資源,這些資料通過軟體的角度去解讀,肯定存在差異的結構,在物理上底層都是陣列。
陣列
陣列,是資料最基本的儲存結構,陣列表示同一種資料有多個。
陣列的有點事查詢快,通過索引能夠快速找到。
但是陣列優缺點,就是修改陣列長度的時候,很麻煩。
示意
陣列 內容為
char[] arrays = {'a','p','p','l','e',' ',' ','1','3','6','6','8','5','$'}
要修改陣列,將arrays[5] = '5' arrays[6] = '$';,然後刪除arrays[6]之後的資料,
最終 arrays變為 arrays = {'a','p','p','l','e','5','$'}
大致過程如下,
使用陣列的時候,當你檢視資料時,特別方便快色,如你要檢視陣列的第2個元素 直接arrays[1]就能訪問獲得元素內容'p'
但是刪除元素的時候卻很麻煩,如上圖刪除index = 5~12的元素,之後你還需要將 index = 13 ~14 的內容搬到 index = 5~6 上面,並且將index等於 7~14的儲存單元清空還給計算機管理(空間高效利用原則)
這還不是最壞的情況,陣列是連續的,如果有一個10000位的陣列,你只是刪除了第一個元素,那麼後面9999個元素都要向前移動一位,這明顯不是很好的資料使用方式,或者多種資料結構在不同使用場景下使用。
連結串列
連結串列是一種帶有下一個元素資訊的資料結構,連結串列有開頭和結尾,並且連結串列不需要一定是連續的儲存單元
連結串列針對陣列元素的新增和刪除,有了明顯直觀的優化,連結串列是不連續的儲存單元儲存資料,每個儲存單元都存有內容+next縮影,通過next索引找到下一個資料
因此刪除資料的時候,只需刪除一個資料,要將next索引修改一下就好了,不會像陣列那樣大規模修改資料。
上面刪除apple 連結串列的 'i'元,將連結串列第三個單元'p'的next 地址修改位指向 e,再刪除'i'即可
連結串列刪除增加很快,但是如果有長度1000的連結串列,檢視第500個元素,只能從第一個開始,通過next定址500此才能找到元素。
樹
樹是一種重要的非線性資料結構,直觀地看,它是資料元素(在樹中稱為結點)按分支關係組織起來的結構,很象自然界中的樹那樣。
度
樹的度——也即是寬度,簡單地說,就是結點的分支數。以組成該樹各結點中最大的度作為該樹的度,如上圖的樹,其度為3;樹中度為零的結點稱為葉結點或終端結點。樹中度不為零的結點稱為分枝結點或非終端結點。除根結點外的分枝結點統稱為內部結點。
深度
樹的深度——組成該樹各結點的最大層次,如上圖,其深度為4;
層次
根結點的層次為1,其他結點的層次等於它的父結點的層次數加1.
路徑
對於一棵子樹中的任意兩個不同的結點,如果從一個結點出發,按層次自上而下沿著一個個樹枝能到達另一結點,稱它們之間存在著一條路徑。可用路徑所經過的結點序列表示路徑,路徑的長度等於路徑上的結點個數減1.
森林
指若干棵互不相交的樹的集合
樹與陣列的區別
查詢一個資料是否存在,
陣列是全部展示,沒有深度,你有索引的情況下能快速查詢,如果沒有索引,需要一個個檢視內容對比。
樹有層次,有深度和寬度,也有索引,索引與元素內容有關聯,如二分查詢法,可以快速得知資料是否存在,而不是每個資料依次檢視。
個人理解樹是有層次的資料結構,有深度和寬度,在某些查詢場景上比陣列資料結構更加高效快速
檔案系統可以看作樹結構的經典應用。
HashMap
可以看出陣列和連結串列是兩種區別很大的資料結構,陣列查詢快,增刪慢,連結串列相反,查詢慢,增刪快
如果能將這兩個資料結構的有點結合,就能很好的去運算元據了,提升效能和資料處理速度,Java中的HashMap就是一個將陣列和連結串列有點結合的資料結構,接下來探索瞭解Java中的HashMap資料結構
存值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
雜湊值
hash值計算是一種獲取不重複值的演算法,通常key不同,返回的數值不同
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public native int hashCode(); //jvm本地方法
索引演算法
putVal(hash(key), key, value, false, true);
// 實際存值程式碼
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// i = (n - 1) & hash i就是計算出的索引值
快速查詢
HashMap類似陣列的快速定位查詢與雜湊計算和陣列有關
下面是HashMap構造和查詢有關的程式碼
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
table 是一個陣列,陣列元素的機構是Node
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; //指向像一個元素,是一種連結串列結構的應用
//... 省略一些程式碼
}
Node 是類似連結串列結構,成員屬性包含一個Node<K,V> next 有next是防止hash碰撞等情況
get方法獲取值(查詢)
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判斷如果table不是空 取值 tab[(n - 1) & hash]
//可以看出陣列索引是 (n-1) & hash 這裡其實是位運算得出陣列索引
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first; //判斷key完全相同則返回內容
if ((e = first.next) != null) {
// 複雜點的情況下 如果發生索引相同衝突,使用連結串列來解決衝突
if (first instanceof TreeNode)
// 如果資料過多會改變連結串列為樹結構,提高資料查詢效率
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
索引衝突
關於HashMap中 table陣列索引的確定(根據key)
hash = hash(key); //通過key獲取hash值
n = tab.length; //獲取陣列長度 值一定是2的n次方(可以看HashMap中resize方法瞭解細節)
index = (n - 1) & hash //在存值和取值的時候,算出index,陣列的索引
table.length 是一個 2的n次方值,也就是說 n-1 一定是 0000011111這種類似結構
n = 8; 二進位制 1000
n - 1 = 7 二進位制 0111
如果一個key的hash值為十進位制數86,二進位制為1010110, 那麼 n=8時
(n-1) & 1010110 = 0000111 & 1010110 它的結果是 0110 換算十進位制為 6,直接找到元素位置table[6];
hashmap在實際使用時可能有key不同,索引值相同的情況。
當兩個key雖然hash值不同,table.length = 8的時候,兩個key的hash值不同,一個值是keyHash1=23001,一個值是keyHash2=20001,這兩個值在(n-1) & hash
的位運算中,結果都是1,所以index = 1,這時候需要用到連結串列去解決這種問題,
table[1] = node1;
node1.key = key1,node1.value = value1 node1.next = node2
node2.key = key2,node2.value = value2 node2.next = null;
連結串列改為樹結構
如果node過多,後面會將連結串列改為樹結構
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 連結串列改為樹的邏輯判斷
treeifyBin(tab, hash);
// TREEIFY_THRESHOLD的定義
static final int TREEIFY_THRESHOLD = 8;
可以知道當連結串列元素超過7個,為了查詢效率,會把當前連結串列結構改為樹結構