數據結構小記

閱前:只是一篇隨手的筆記(內容參考來源書籍:數據結構與算法、算法導論、算法精解、算法圖解等書籍),幫助本身記錄學習過程,順便留些坑。
 
遵循後進先出原則的有序集合。
生產:
                            → +3 | 3 |+2 | 2 |      | 2 |
|   | → +1 | 1 |      | 1 |      | 1 |
消費:
| 3 | → -3
| 2 |      | 2 | → -2
| 1 |      | 1 |      | 1 | → -1 |   |
 
隊列
遵循先進先出原則的有序集合。
生產:
                            → +3 | 3 |+2 | 2 |      | 2 |
|   | → +1 | 1 |      | 1 |      | 1 |
消費:
| 3 |
| 2 |      | 3 |
| 1 | → -1 | 2 | → -2 | 3 | → -3 |   |
若是要優先隊列,在上面的結構上加入 priority ,根據 priority 排序。
// 好比 值越小優先級越高
| 1 : priority 3 |
| 2 : priority 2 |
| 3 : priority 1 |

 

鏈表
鏈表存儲的是一系列有序的元素集合,每一項在內存中能夠不是連續放置的,經過當前元素的屬性引用下一個元素(或加上上一個元素的引用,成雙向鏈表)內存地址便可。
其結構以下:
 -------         -------         -------
| v : 1 |   →   | v : 2 |   →   | v : 3 |
| next  |  next | next  |  next | next  |
 -------         -------         -------
或者雙向鏈表:
 --------          --------          --------
| v : 1  |  next  | v : 2  |  next  | v : 3  |
| next   |   →    | next   |   →    | next   |
| return |   ←    | return |   ←    | return |
 --------  return  --------  return  --------
 
小記:又如 react hooks,或者 fiber 對象鏈(這個鏈表有點畸形)
hooks 使用代碼:
const [num, setNum] = useState(1);
const [str, setStr] = useState("a");
fiber 對象上的 memoizedState 屬性(hooks):
 --------------------
|   baseState : 1    |
|  baseUpdate : null |
|  memoizedState : 1 |
|       next         |
|       queue        |
 --------------------
     next ↓
 --------------------
|   baseState : "a"  |
|  baseUpdate : null |
| memoizedState : "a"|
|       next         |
|       queue        |
 --------------------
     next ↓
 --------------------
|   baseState : null |
|  baseUpdate : null |
|    memoizedState   |
|      next : null   |
|      queue : null  |
 --------------------

 

集合
表示一組裏面的項是無序且惟一的元素,由 `{ }` 表示。
 
ECMAScript 6 裏的 Set:
const mySet = new Set([1, 2, 3, 3, 4, 5, 6, 7]);
// Set(7) {1, 2, 3, 4, 5, 6, 7}
並集 & 交集 & 差集 & 子集
A ∪ B : [ 1, 2, 3 ] ∪ [ 2, 3, 4 ] => [ 1, 2, 3, 4 ]
A ∩ B : [ 1, 2, 3 ] ∩ [ 2, 3, 4 ] => [ 2, 3 ]
A - B : [ 1, 2, 3 ] - [ 2, 3, 4 ] => [ 1 ]
B - A : [ 2, 3, 4 ] - [ 1, 2, 3 ] => [ 4 ]
B ⊆ A : [ 2, 3 ] ⊆ [ 1, 2, 3 ] => [ 2, 3 ]
ES6 Set & WeakSet 具體的本身看 https://www.ecma-international.org/ecma-262/6.0/
 
字典
表示一組無序且惟一的[鍵, 值]對,經過鍵來查詢值,也由 `{}` 表示。

ECMAScript 6 裏的 Map:
const myMap = new Map([["1", 1], [1, "1"]]);
// Map(2) {"1" => 1, 1 => "1"}
ES6 Map & WeakMap 具體的本身看 https://www.ecma-international.org/ecma-262/6.0/
 
散列表
在數組中,通常須要知道下標纔可以直接取到須要的值(O(1),一次操做),若是使用關鍵字查找,那麼最壞須要 N 次(N 是數組長度,也就是 O(n));而散列表能夠將經過關鍵字搜索降到 O(1),其原理是經過散列函數將關鍵字轉換成存儲數據的下標存在在散列表中,當經過關鍵字搜索時,只須要經過散列函數取得該下標就能獲得值(固然是理想狀況下的 O(1),實際實現中大多仍是 O(n),只不過它可以將其搜索範圍縮小,起碼 java 和 redis 裏的 hashMap 的對應存儲值是鏈表形式的...)。
 
在散列表中,經過散列函數,將搜查 key 數據的下標,結構以下:
 ---------------------------------------------------------
|   key   |  hash fn   |   hash key  |     hash table     |
 ---------------------------------------------------------
|   "aa"  |  fn("aa")  |     149     | [149] aa@gmail.com |
 ---------------------------------------------------------
|   "bb"  |  fn("bb")  |     153     | [153] bb@gmail.com |
 ---------------------------------------------------------
|   "cc"  |  fn("cc")  |     157     | [157] cc@gmail.com |
 ---------------------------------------------------------
這裏以取模求散列值的方法爲例,用數組來存儲數據:
//
var table = [];

// 舉個例子,好比一個簡單的散列計算函數
var hashCode = key => {
  let hash = 0;
  for (let i = 0; i < key.length; i++) {
    hash = hash + key.charCodeAt(i);
  }
  return hash;
};

// 存值
var put = (key, value) => {
  var position = hashCode(key);
  table[position] = value;
};

// 取值
var query = key => {
  var position = hashCode(key);
  return table[position];
};

put("hy", "hello yeshou");
put("ht", "hello tutu");
put("hw", "hello world");
console.log(query("hy")); // output hello yeshou
console.log(query("ht")); // output hello tutu
console.log(query("hw")); // output hello world

put("yh", "hello haha");
console.log(query("hy")); // output hello world
console.log(query("yh")); // output hello world
然而上面的代碼是有問題的,當把 hy 倒過來 yh 的時候,hash 的計算是同樣的值,因而會出現後面的現象,這就是 hash 函數計算的衝突了,更好的 hash 函數能夠下降衝突率,好比網上的一些  lose losedjb2 , sdbm 等..
 
djb2 :
var hashCode = key => {
  let hash = 5381; // 初始 hash 值
  for (let i = 0; i < key.length; i++) {
    hash = hash * 33 + key.charCodeAt(i); // 乘積因子 33
  }
  return hash % 1013; //  mod 5381
};

console.log(hashCode("yh"), hashCode("hy")); // output 762 218
我的認爲這種方式生成的 hashMap 空間方面會比較多的無效佔用(好比上面的 hashMap length=1012),但存的數據根據我但願的只須要有 78 個便足夠,除非設計者自己就須要存這麼多的數據,否則的話能夠根據需求去設計初始的 hash 值、乘積因子和 mod 。(保留意見,暫時對這方面研究深度不夠,僅作筆記)

除了精心設計的 hash 函數可以下降衝突率,一些書籍(如算導、數據結構、算法精解)中還有提到一些"改進 hashMap 存儲規則的方法"來解決可能出現的衝突,如"連接法"和"開放尋址法"。這裏僅簡單提一下.

連接法:保留衝突,在衝突的項上建立是一個雙向鏈表(算導裏推薦的是雙向鏈表,操做的負責度爲 O(1)),能夠簡單理解爲 Map+鏈表 的結合,在查找過程當中能夠快速定位查找範圍,節省沒必要要部分的檢索;

開放尋址法:保留衝突,在衝突位置根據指定的探查方式對散列表進行探查,直到探查到空槽來放置元素;元素都存在散列表裏,裝載因子 α 不會超過 1;三種經常使用的計算開放尋址法的探查序列:線性探查、二次探查、雙重散列。

小記:java hashMap 使用的是連接法,在 JDK-1.8 以前由 load factor = 0.75 (裝載因子 > 0.75)爲界限執行擴容;1.8 後以 TREEIFY_THRESHOLD = 8 (鏈表長度 > 8)爲界限執行轉換紅黑樹。(保留筆記,以後有興趣再探索擴容和轉換過程)
 
樹是一種分層數據的抽象模型,一個樹結構包括一系列的存在父子關係的節點,每一個節點(除了根節點)都有一個父節點以及零個或多個子節點。
 
以下結構:
               (root)
                 10
           /           \
          8             9
    /  /  |  \  \       |
  1   2   4   5  7      15
 
二叉樹 & 二叉搜索樹
二叉樹是每一個結點最多有兩個子節點(樹)的樹結構。
二叉搜索樹是二叉樹的一種,規則是左側子節點的值小於自己節點的值,右側節點的值大於自己節點的值(使得其操做的複雜度等於樹高)。
二叉搜索樹結構以下:
                        (root)
                          11
             /                         \
            7                           15
      /           \               /           \
     5             9             13            17
   /   \         /    \       /      \      /     \
  4     6       8     10     12      14    16      18
 
二叉樹的遍歷
 
先序遍歷:以優先於後代節點的順序訪問每一個節點,日常的遍歷操做順序就是這種。
代碼以下:
function traverse(node, callback) {
  if (node !== null) {
    callback(node.v);
    traverse(node.left, callback);
    traverse(node.right, callback);
  }
}
若是拿上面的結構來講,順序是 11,7,5,4,6,9,8,10,15,13,12,14,17,16,18
 
中序遍歷:一種以上行順序訪問樹中全部節點的遍歷方式,以從最小到最大的順序訪問全部節點。
代碼以下:
function traverse(node, callback) {
  if (node !== null) {
    traverse(node.left, callback);
    callback(node.v);
    traverse(node.right, callback);
  }
}
若是拿上面的結構來講,順序是 4,5,6,7,8,9,10,11,12,13,14,15,16,17,18
 
後序遍歷:先訪問節點的後代節點,再訪問節點自己。(應用:如計算一個目錄和其子目錄中全部文件所佔空間大小)
代碼以下:
function traverse(node, callback) {
  if (node !== null) {
    traverse(node.left, callback);
    traverse(node.right, callback);
    callback(node.v);
  }
}
若是拿上面的結構來講,順序是 4,6,4,8,10,9,7,12,14,13,16,18,17,15,11
 
紅黑樹
紅黑樹是一顆二叉搜索樹,它在每一個節點上增長了一個存儲位以表示節點的顏色(只能夠是紅色和黑色),經過對任何一條從根到葉子的簡單路徑上各個節點的顏色進行約束,確保沒有一條路徑會比其餘路徑長出 2 倍,於是保證此樹是近似於平衡的(主要是保持樹的平衡,可減短特殊狀況下的操做路徑)。
 
紅黑樹知足如下規則:
  1. 根節點是黑色的;
  2. 每一個節點只能是紅色或者黑色的;
  3. 每一個葉子節點(nil)是黑色的;
  4. 若是一個節點是紅色的,則它的兩個子節點是黑色的;
  5. 對每一個節點,從該節點到其全部後代葉節點的簡單路徑上均包含相同數目的黑色節點。

對於紅黑樹的操做,會根據規則作一些改變(樹節點的變色和旋轉),以下案例:
樹中的 n 爲 nil,黑色 ; B 爲黑色 ; R 爲紅色。
                        (root)
                          13(B)
             /                         \
           8(R)                        17(R)
      /           \               /           \
     1(B)         11(B)         15(B)          23(B)
   /   \         /    \       /      \       /      \
  n    6(R)     n      n     n        n     22(R)   25(R)

//  若是僅插入 14
                        (root)
                          13(B)
             /                         \
           8(R)                        17(R)
      /           \               /           \
     1(B)         11(B)         15(B)          23(B)
   /   \         /    \       /      \       /      \
  n    6(R)     n      n     14(R)    n     22(R)   25(R)
                             / \
                            n   n

//  若是僅插入 21
                        (root)
                          17(B)
             /                             \
           8(R)                            23(R)
      /           \                   /           \
     1(B)         13(B)             22(B)          25(B)
   /   \         /    \           /      \       /      \
  n    6(R)     11(R) 15(R)     21(R)    n     n        n
       / \      / \    / \       / \
      n   n    n   n  n   n     n   n

//  若是僅刪除 15
                        (root)
                          13(B)
             /                             \
           6(R)                            23(R)
      /           \                   /           \
     1(B)         8(B)              22(B)          25(B)
   /   \         /    \           /      \       /      \
  n     n     n        n         n        n     n        n

 

B 樹 & B+樹
B 樹將任何和關鍵字相關聯的"數據指針"和關鍵字一塊兒存放在節點中。
 
其結構規則以下:
  1. 每一個節點 x 具備下面屬性;
    - x.n,當前存儲節點中的關鍵字個數;
    - x.n 個關鍵字自己:x.key_1, x.key_2, ..., x.key_x.n,以非降序存放,使得:x.key_1 <= x.key_2 <= ... <= x.key_x.n;
    - x.leaf,一個 boolean 值,若是 x 是葉節點,則是 ture;若是 x 是內部節點,則爲 false;
  2. 每一個內部節點 x 還包含 x.n+1 個指向其子節點的指針:x.c_1, x.c_2, ..., x.c_x.n+1,葉節點沒有子節點,因此其 c_i 屬性沒有定義;
  3. 關鍵字 x.key_1 對存儲在各個字數中的關鍵字範圍進行分割:若是 k_i 爲人任意一個存儲在以 x.c_i 爲根的子樹中的關鍵字,那麼:k_1 <= x.key_1 <= k2 <= x.key_2 <= ... <= x.key_x.n+1 <= k_x.n+1
  4. 每一個葉節點具備相同是深度,即樹的高度 h;
  5. 每一個節點所包含的關鍵字個數有上界和下界,用 B 樹的最小讀書的固定整數 t >= 2 來表示這些界;
    - 除了根節點之外的每一個節點必須至少有 t-1 個關鍵字,所以除了根節點之外的每一個內部節點至少有 t 個子節點;若是樹非空,根節點至少有一個關鍵字;
    - 每一個節點至多可包含 2t-1 個關鍵字,所以一個內部節點至多可有 2t 個子節點,當一個節點剛好有 2t-1 個關鍵字時,則該節點是滿了;

B+樹將"數據指針"存在葉節點中,內部節點只存放關鍵字和子節點的指針,所以最大化了內部節點的分支因子。
 
小記:聽說 mysql 的索引聽說 B+樹實現的... 留坑,閒時再作一次對 mysql 的深度探索。
 
若是說樹是分層的抽象,那圖就更加抽象了,針對結構的抽象模型,由於任何二元關係均可以用圖來表示。
一個圖結構由點(節點)和線(邊)組成。
 
以下:
// 無(方)向圖
A - B - C - D
|   |       |
E   F - G - H
        |   |
         -I-

// 有(方)向圖
A → B → C → D
↓   ↓       ↓
E   F → G → H
        ↓   ↓
         →I←

 

圖的遍歷能夠用來尋找特定的節點或尋找兩個節點之間的路徑,檢查節點之間是否連通或者是否含有環等。java

搜索:深度優先 & 廣度優先
 
如上述圖,深度優先搜索(Depth-First Search)的順序是:
A E
A B F G I
A B F G H I
A B C D H I

 

如上述圖,廣度優先搜索(Breadth-First Search)的順序是:
A E
A B C
A B F
A B C D
A B F G
A B C D H
A B F G I
A B F G H
A B C D H I
A B F G H I
 
搜索過程是拆分紅每次搜索步驟的,可能這樣描述也不是很好理解... 能理解的就理解吧,不能理解的也確實不理解...
通常狀況下深度搜索佔用內存少但速度較慢,廣度搜索佔內存較多但速度較快。
深度優先和廣度優先都有對應的應用場景(我的看法,這塊還缺少豐富的實踐經驗):好比要搜索單個值或特定值,那麼深搜是相對較好的方式;好比搜索近似值或全部值,則廣搜更具"全局"的概念。

小記:深搜廣搜對於樹也是適用的。同時這塊深刻進去還能夠帶"記憶"的搜索(緩存搜索過程和結果),動態規劃等知識... 這塊後續會有給出筆記,含分治策略等~
 
最後
但願本身技術水平愈來愈好吧 ~(・ェ・。)~ 越學越以爲本身渣...

本文內容僅作學習參考,更多詳情自行官方學習;佛系寫文,不喜勿噴。
相關文章
相關標籤/搜索