數組
定義:數組是種線性表數據結構,他用一組連續的內存空間,來存儲一組具有相同類型的數據。對內存的要求比較高
首先是線性表:每個數據只有前後倆個方向
連續的內存空間和相同的數據類型:可以支持下標隨機訪問。
插入操作:O(n),由於涉及到後續數組元素的遷移
如果是無序的話,假設現在要向 數組a中第三個元素後插入一個數據,我們可以直接將第四個元素一直數組末尾,然後將數據放入第四個元素,時間複雜度爲O(1)
刪除操作:O(n) ,但刪除一個元素後,後續的元素需要進行數據的搬移。
優化:我們可以先將該位置的元素標記爲已刪除,當數組的空間不夠時,可以觸發一次真正的操作,這樣就減少了數組的搬移。java中的標記回收垃圾算法於此類似。
鏈表
定義:通過指針將零散的內存塊串聯起來使用。對內存要求比較低。
節點:內存塊
後繼指針next:記錄下一個節點的位置
頭結點:鏈表的第一個節點
尾節點:鏈表的最後一個節點
單鏈表:每個節點有一個next節點,尾節點指向null。複雜度:查詢O(n ) , 刪除:O(1),插入O(1)
循環鏈表:在單鏈表的基礎上,尾節點不指向null ,而是指向頭結點
雙向鏈表:在單鏈表的的基礎上,每個節點加了pre 節點,指向上一個節點
常用的算法以及解決:
迴文字符串:使用快慢兩個指針找到鏈表中點,慢指針每次前進一步,快指針每次前進兩步。在慢指針前進的過程中,同時修改其 next 指針,使得鏈表前半部分反序。最後比較中點兩側的鏈表是否相等。
所有的數據結構都可以歸結爲倆個數據結構,一個是數組,一個是鏈表
列數組 | 列表 | |
---|---|---|
優勢 | 隨機訪問 | 只能順序訪問 |
劣勢 | 需要連續的空間 | 需要的空間可以不連續 |
棧
定義:棧主要包含兩個操作, 入棧和出棧, 也就是在棧頂插入一個數據和從棧頂刪除一個數據。先進後出
出棧與入棧的複雜度均爲O(1)比如
應用
函數調用棧,用來存儲函數的參數以及返回值等
求運算表達式:
實際上, 編譯器就是通過兩個棧來實現的。 其中一個保存操作數的棧, 另一個是保存運算符的棧。我們從左向右遍歷表達式, 當遇到數字, 我們就直接壓入操作數棧; 當遇到運算符, 就與運算符棧的棧頂元素進行比較。
如果比運算符棧頂元素的優先級高, 就將當前運算符壓入棧; 如果比運算符棧頂元素的優先級低或者相同, 從運算符棧中取棧頂運算符, 從操作數棧的棧頂取 2 個操作數, 然後進行計算, 再把計算完的結果壓入操作數棧, 繼續比較。
用來判斷()表達式是否正確。
比如, {[{}]}或 [{()}([])] 等都爲合法格式, 而{[}()] 或 [({)] 爲不合法的格式。 那我現在給你一個包含三種括號的表達式字符串, 如何檢查它是否合法呢?
這裏也可以用棧來解決。 我們用棧來保存未匹配的左括號, 從左到右依次掃描字符串。 當掃描到左括號時, 則將其壓入棧中; 當掃描到右括號時, 從棧頂取出一個左括號。 如果能夠匹配, 比如“(”跟“)”匹配, “[”跟“]”匹配, “{”跟“}”匹配, 則繼續掃描剩下的字符串。 如果掃描的過程中, 遇到不能配對的右括號, 或者棧中沒有數據, 則說明爲非法格式。
當所有的括號都掃描完成之後, 如果棧爲空, 則說明字符串爲合法格式; 否則, 說明有未匹配的左括號, 爲非法格式
實現
public class ArrayStack {
private String [] items;
private int count; //棧內數量
private int size;
public ArrayStack(int size){
items = new String[size];
count = 0;
this.size = size;
}
public boolean push(String param){
if(size == count){
return false;
}
items[count++] = param;
return true;
}
public String pop(){
if(count ==0){
return null;
}
return items[--count];
}
}
隊列:
定義:隊列跟棧一樣, 也是一種抽象的數據結構。 它具有先進先出的特性, 支持在隊尾插入元素, 在隊頭刪除元素
實現:
public class ArrayQueue<T> {
private Object[] items;
private int tail ;
private int head;
private int size;
public ArrayQueue(int size){
this.size = size;
tail = head = 0 ;
items = new Object[size];
}
public boolean enqueue(T t){
//隊列滿
if(tail == size && head == 0){
return false;
}
//由於不斷的進行入隊和出隊,導致head 一直向後移,當tail 指向末尾時 , 此時需要進行數據的遷移
if(tail == size){
for (int i=head;i<=tail;i++){
items[i-head] = items[i];
}
tail = tail - head;
head = 0 ;
}
items[tail++] = t;
return true;
}
public T dequeue(){
if(tail == head){
return null;
}
return (T) items[head++];
}
}
循環隊列:
重點是判空與隊列是否滿,判空與上述一樣是 tail == head, 判斷是否滿:(tail + 1)%n =head; 會浪費一個空間。。
public boolean enqueue(T t){
//隊列滿
if((tail+1)% size == head){
return false;
}
items[tail]= t;
tail = (tail+1) %size ;
return true;
}
public T dequeue(){
if(tail == head){
return null;
}
Object item = items[head];
head = (head+1) %size;
return (T) item;
}
併發隊列和阻塞隊列。
應用:
線程池,對象池,連接池
跳錶:
對於一個單鏈表來講, 即便鏈表中存儲的數據是有序的, 如果我們要想在其中查找某個數據, 也只能從頭到尾遍歷鏈表。 這樣查找效率就會很低, 時間複雜度會很高, 是 O(n)
可以實現快速查找,插入和刪除。
分析:
空間複雜度爲O(n)
查詢、刪除、添加的時間複雜度爲O(logn)
問題:當我們不停的向跳錶中插入數據是,如果不更新索引,就有可能出現2個索引節點之間的數據非常多,甚至可能會退化成單鏈表。
當我們往跳錶中插入數據的時候,我們可以選擇同時將這個數據插入到部分索引層中。如何選擇加入哪些索引層呢?
我們通過一個隨機函數,來決定將這個結點插入到哪幾級索引中,比如隨機函數生成了值 K,那我們就將這個結點添加到第一級到第 K 級這 K 級索引中。
散列表
散列表用的是數組支持按照下標隨機訪問數據的特性, 所以散列表其實就是數組的一種擴展, 由數組演化而來。 可以說, 如果沒有數組, 就沒有散列表。一般是通過對key 來進行hash,然後通過特定的算法 根據hash找到對應的下標,然後放入。
散列函數設計原則:
1 經過hash後得到的值爲非負數,因爲數組的的下標爲非負數
2 如果key1 == key1 ,那麼hash(key1) == hash(key2)
3 如果key1 != key2 , 那麼hash(key1) != hash(key2) //這個很難保證
散列衝突解決方法:
1 開放尋址法:如果經過計算得到的hash值對應的下標已經有數據了,我們可以重新探測一個位置。
探測位置的方法:
直接向後找,如果有空的,就插入。
二次探測 : 跟線性探測很像, 線性探測每次探測的步長是 1, 那它探測的下標序列就是hash(key)+0, hash(key)+1, hash(key)+2……而二次探測探測的步長就變成了原來的“二次方”, 也就是說, 它探測的下標序列就是 hash(key)+0, hash(key)+1*1 , hash(key)+2*2 ……
雙重散列:使用多個hash函數,如果第一個哈希函數計算得到的已經有值,那麼再使用第二個,知道找到符合條件的
但是使用了開放尋址法,刪除就不能直接刪除了,直接刪除會對查詢造成影響,我們只可以對該元素標記爲已刪除。
適用於裝載因子很小,數據量很小的場景,java 的 threadLocalMap 使用的就是該方法
2 拉鍊法 :將數組中每個下標對應的元素都設置爲鏈表,如果有衝突,就放入鏈表。優化:可以使用跳錶,紅黑樹等數據結構來代替鏈表,即使最後所有數據全放在一個bucket中,查詢效率優勢logN,而不是O(n)。java中的HashMap使用的就是拉鍊法,當鏈表中元素個數大於8的時候,就會轉化爲紅黑樹,小於8的時候,紅黑樹會轉化爲鏈表
降低擴容效率:
如果負載因子過大的話,此時需要進行擴容,使用一次性擴容,會導致某次插入變得很慢,我們可以使用分批次的擴容。
當裝載因子觸達閾值之後, 我們只申請新空間, 但並不將老的數據搬移到新散列表中。當有新數據要插入時, 我們將新數據插入新散列表中, 並且從老的散列表中拿出一個數據放入到新散列表。 每次插入一個數據到散列表, 我們都重複上面的過程。 經過多次插入操作之後, 老的散列表中的數據就一點一點全部搬移到新散列表中了。 這樣沒有了集中的一次性數據搬移, 插入操作就都變得很快了。
查詢的話,可以先去新的哈希表中進行查詢,查不到的話,在去老的中查。
散列表這種數據結構雖然支持非常高效的數據插入、刪除、查找操作,但是散列表中的數據都是通過散列函數打亂之後無規律存儲的。也就說,它無法支持按照某種順序快速地遍歷數據。如果希望按照順序遍歷散列表中的數據,那我們需要將散列表中的數據拷貝到數組中,然後排序,再遍歷。如果我們希望散列表可以有某種順序,那麼可以將散列表和鏈表結合起來。例如:java中的LinkedHashMap ,就是利用雙向鏈表和散列表組合得。redis有序集合使用的是跳錶和散列表結合
哈希算法
將任意長度的二進制字符串可以映射爲固定長度的二進制值
要求:
- 不能從hash值逆推出源數據
- 數據發生一點修改,hash值都不同
- 衝突的概率要小
- 執行效率高效
應用:
- 安全加密, 加密他人密碼, 適度放入salt
- 唯一標識,可以爲文件用作唯一標識
- 數據檢驗 ,可以檢驗文件是否被修改過
- 散列函數
- 負載均衡 ip hash
- 數據分片 通過求出數據的hash,對機器個數進行取模,將對應的數據塞入對應的機器進行處理
- 分佈式存儲 對文件進行hash,存儲到對應的文件中。這裏可以使用一致性hash(假設我們有 k 個機器, 數據的哈希值的是[0, MAX]。 我們將整個範圍劃分成 m個小區間(m遠大於 k) , 每個機器負責 m/k 個小區間。 當有新機器加入的時候, 我們就將某幾個小區間的數據,從原來的機器中搬移到新的機器中。 這樣, 既不用全部重新哈希、 搬移數據, 也保持了各個機器上數據數量的均衡。)