目錄
數組
數組是一種線性表數據結構,它用一組連續的內存空間,來存儲一組具有相同類型的數據
數組,鏈表,隊列,棧都是線性表結構
非線性表結構有 樹,二叉樹,堆,圖等
數組下標從0開始,確切定義是偏移offset,用a來表示數組首位地址,a[0]就是偏移爲0的位置
a[k]表示k個type_size 位置,計算a[k]內存地址的公式
a[k]_address = hbase_address + k * type_size
如果下標從1開始,則計算a[k]內存地址就變爲
a[k]_address = hbase_address + (k-1)*type_size
數組的 O(1)插入
如果數組不要求有序,假設插入到第k個位置,可以先將第k位的元素移到數組最後,
再將新元素插入到第k位
假設數組中有a,b,c,d,e幾個元素,將x插入到第三個位置,只需要將c移到a[5],結果就是
a,b,x,d,e,c
如果刪除時不要求數據一定連續,可以將多次的刪除操作合併到一起執行,提高效率
這就是JVM標記清除垃圾算法的核心
一段死循環代碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int i = 0;
int arr[3] = {0};
for(;i<=3;i++) {
arr[i] = 0;
printf("hello world\n");
}
return 0;
}
棧是從高到低增長的,所以棧中的元素順序是i,a[2],a[1],a[0]對如下代碼
int i = 0;
int j = 1;
int k = 2;
int arr[3] = {0};
cout<<"i-"<<&i<<endl;
cout<<"j-"<<&j<<endl;
cout<<"k-"<<&k<<endl;
cout<<"arr-"<<&arr<<endl;
cout<<"arr3-"<<&arr[3]<<endl;
運行結果:
i-0x28ff0c
j-0x28ff08
k-0x28ff04
arr-0x28fef8
arr3-0x28ff04
鏈表
- 單鏈表
- 循環單鏈表(約瑟夫環問題)
- 雙鏈表
- 雙向循環鏈表
幾個寫鏈表的技巧
- 理解指針或引用的含義
- 警惕指針丟失和內存泄露
- 利用哨兵建好實現難度
- 重點留意邊界條件處理
- 舉例畫圖輔助思考
5個常見的鏈表操作
- 單鏈表反轉
- 鏈表中的循環檢測
- 兩個有序的鏈表合併
- 刪除鏈表倒數第n個節點
- 求鏈表的中間節點
其他
一個字符串中是否有迴文字符串
單鏈表存儲的字符串,如何判斷迴文
棧
- 順序棧
- 鏈式棧
- 支持動態擴容的順序棧
根據均攤分析動態擴容的順序棧時間複雜度是O(1)
棧的實際應用
- 函數調用棧
- 表達式求值
- 括號匹配中的應用
模擬瀏覽器前進後退功能
- 使用兩個棧X,Y
- 首次瀏覽的頁面壓入X棧
- 點擊後退時,依次從X棧彈出放到Y棧中
- 通過頁面b又跳轉到新頁面d,頁面C就無法通過前進後退按鈕重複查看了,要清空棧Y
隊列
也是一種操作受限的線性表數據結構
- 順序隊列
- 鏈式隊列
- 循環隊列
- 阻塞隊列
- 併發隊列
順序隊列當tail指針移動到數組最右邊後,如有新數據入隊,可以將head到tail之間的數據
整體搬移到數組中0到tail-head的位置
循環隊列實現的關鍵,確定好隊空和隊滿的條件
跳錶
對鏈表的改造,可以支持二分查找的鏈表
可以替代紅黑樹的動態數據結構
在原始的節點之上,增加了一層,兩層,多層的索引,提高搜索效率
下圖是一個64個節點的鏈表,有5層索引,如果搜索62個節點需要遍歷62次,現在只需要11次
這種鏈表加多級索引的結構,就是跳錶
假設第一級索引是n/2,第二級是n/4,第k級索引節點是n/(2^k)
假設每一層要遍歷m次,跳錶的時間複雜度是O(m*logn)
可以算出每一層只需要遍歷三次,也就是m=3,所以時間複雜度是O(logn)
跳錶的空間複雜度爲O(n)
如果每3個或者5個節點,抽一個節點到上級索引,其空間複雜度大概能降低一半
實際開發中,鏈表中的數據可能很大,索引節點存儲的只是指針,所以空間可以忽略
跳錶中刪除/插入操作,需要先找到這個節點/或者前驅節點,再執行操作
找到某個節點的操作時間是O(logn),插入和刪除的操作時間是O(1),所以總的時間就是O(logn)
如果不停的往跳錶中插入數據,不更新索引,可能出現2個索引節點直接數據非常多,極端情況下就退化成了單鏈表
類似AVL樹和紅黑樹的左右旋轉操作,跳錶是通過一個隨機函數,來決定這個節點插入到哪幾級索引中
如果隨機函數生產了值K,就將這個節點添加到第一級到第K級索引中
這裏的隨機函數選擇就很有講究了,需要從概率上保證跳錶索引大小和數據大小平衡,不至於性能過度退化
Redis中的有序集合是通過跳錶來實現的(還用到了散列表):
- 插入一個數據
- 刪除一個數據
- 查找一個數據
- 按區間查找數據(如查到[100,356]之間的數據)
- 迭代輸出有序數據
紅黑樹可以完成1,2,3,5但是第4點就不行了
跳錶可以用O(logn)時間定位到一個指定的值如100,然後遍歷這個鏈表後續的值就可以了
散列表
- 散列函數的設計,不能太複雜,生成的值要隨機均勻
- 裝載因子過大後支持動態擴容(小於某個閾值可以縮容)
- 散列衝突,開放尋址 和 鏈表
開放尋址vs鏈表
- 數據量小時可以採用開放尋址法,Java的ThreadLocalMap使用了,同一個數組中利用cpu緩存
- 鏈表法適合存儲大對象,大數據量,當鏈>8啓動紅黑樹,當鏈<8退回爲鏈表
開放地址法
包括普通的線性探測
二次探測
雙重散列
當刪除一個元素時,不能直接刪除,否則線性探測發現這個位置爲空就會判斷失敗,得加上deleted標誌
Java的HashMap散列函數
int hash(Object key) {
int h = key.hashCode();
//這裏使用了異或,和位移,計算出來的具有高低位性質,同時用 & 模擬取餘運行達到均勻分佈
return (h ^ (h >>> 16)) & (capitity -1);
}
避免低效擴容
如果散列表已經有1G了,此時空間不夠再擴容一倍變成2G,而且所有的key都需要重新計算散列函數
可以先申請2G的空間,但不做搬移操作,新的key插入到新的散列表中,再從老表中拿一個key重新計算後放入新表
查詢時爲了兼容老的,需要先從新的中查詢如果沒有再去老的中查詢
這樣的均攤方法,將一次性擴容的代價,均攤到多次插入操作中
散列表+鏈表
LRU緩存淘汰機制,需要用到散列表+鏈表的方式,結構如下
鏈表中的數據節點data,還有前驅節點prev,後驅節點next,新增了一個特殊節點hnext
前驅和後驅針織是爲了將節點串在雙向鏈表中
hnext指針是爲了將節點串在散列表的拉鍊中
按照這個圖的原理,查找,刪除,增加都是O(1)時間複雜度
Redis有序集合中,有兩個重要屬性key鍵值 score分值
可以通過用戶ID來查找積分信息,也可以通過積分區間來查找用戶ID或者姓名
Redis有序集合包括如下操作
- 添加一個成員對象
- 按照鍵值來刪除一個成員對象
- 按照鍵值來查找一個成員對象
- 按照分值區間查找數據,如查找積分在[100,356]之間的成員對象
- 按照分值從小到大排序成員變量
以上需求如果只是用跳錶就不行了,需要用鏈表+散列表的方式纔可以
Java的LinkedHashMap 也是類似的散列表+鏈表的實現方式
底層就是HashMap,又加了一個雙向鏈表,通過雙向鏈表維持插入順序
LinedHashMap也支持按訪問順序來操作元素,當一個元素被訪問時,就將其放到鏈表末尾,但散列表中的位置不動
下面是操作Java的LinkedHashMap的三個操作
- 第一次將4個元素put到map中,鏈表的結構如下
- 第二次修改key爲3的值,於是將key爲3的元素放到鏈表末尾
- 第三次訪問key爲5的元素,於是將key爲5的元素放到鏈表末尾
參考