基本數據結構

數組

定義:數組是種線性表數據結構,他用一組連續的內存空間,來存儲一組具有相同類型的數據。對內存的要求比較高

首先是線性表:每個數據只有前後倆個方向

連續的內存空間和相同的數據類型:可以支持下標隨機訪問。

插入操作: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有序集合使用的是跳錶和散列表結合

哈希算法

將任意長度的二進制字符串可以映射爲固定長度的二進制值

要求:

  1. 不能從hash值逆推出源數據
  2. 數據發生一點修改,hash值都不同
  3. 衝突的概率要小
  4. 執行效率高效

應用:

  1. 安全加密, 加密他人密碼, 適度放入salt
  2. 唯一標識,可以爲文件用作唯一標識
  3. 數據檢驗 ,可以檢驗文件是否被修改過
  4. 散列函數
  5. 負載均衡 ip hash 
  6. 數據分片 通過求出數據的hash,對機器個數進行取模,將對應的數據塞入對應的機器進行處理
  7. 分佈式存儲 對文件進行hash,存儲到對應的文件中。這裏可以使用一致性hash(假設我們有 k 個機器, 數據的哈希值的是[0, MAX]。 我們將整個範圍劃分成 m個小區間(m遠大於 k) , 每個機器負責 m/k 個小區間。 當有新機器加入的時候, 我們就將某幾個小區間的數據,從原來的機器中搬移到新的機器中。 這樣, 既不用全部重新哈希、 搬移數據, 也保持了各個機器上數據數量的均衡。)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章