關於資料結構

吳楠予發表於2020-10-27

資料結構

本文準備講一下軟體開發中的資料結構。

物理儲存

因為資料結構是用來儲存資料的具體方式,在將資料結構之前,說說資料物理儲存。

平時軟體開發中,一個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個,為了查詢效率,會把當前連結串列結構改為樹結構

相關文章