整理和自己總結的部分數據結構和算法。
數據結構
隊列
特點是FIFO。是一種常見的數據結構。可用鏈表和數組實現。
出隊時,鏈表只需要給出鏈頭並將鏈頭重新指向即可,而數組則需要進行一次全數組移動的操作。
入隊時,鏈表需要遍歷一遍鏈表,數組則不需要。
擴容時,鏈表是需要遍歷一次,而數組需要進行一次拷貝。
操作 | 鏈表實現複雜度 | 數組實現複雜度 |
---|---|---|
出隊 | 1 | n |
入隊 | n | 1 |
擴容 | n | n |
樹
樹的子樹還是樹;
度:節點的子樹個數;
樹的度:樹中任意節點的度的最大值;
兄弟:兩節點的parent相同;
層:根在第一層,以此類推;
高度:葉子節點的高度爲1,根節點高度最高;
有序樹:樹中各個節點是有次序的;
森林:多個樹組成;
二叉樹
二叉樹的數組存儲下標公式:第i個節點的左右子節點是2i+1, 2i+2
滿二叉樹的深度爲k,節點數是2的k次方-1
先序遍歷:先訪問根節點,再訪問左節點,再訪問右節點
中序遍歷:左->根->右
後序遍歷:左->右->根
中序遍歷+另外一種遍歷可以還原一棵二叉樹。
二叉查找樹:
1. 左子樹的所有節點值都小於根節點值,右子樹所有節點值都大於根節點值。
2. 左右子樹也爲二叉查找樹。
3. 所有節點值都不重複。
4. 中序遍歷是遞增序。
5. 插入和刪除操作。
紅黑樹 (有點看不懂,先放着)
某blog
一種平衡二叉查找樹。C++ stl 中set, multiset, map , multimap(muti允許內部有重複元素)用了紅黑樹。
紅黑樹是每個節點都帶有顏色屬性的二叉查找樹,顏色或紅色或黑色。在二叉查找樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求:
1. 節點是紅色或黑色。
2. 根節點是黑色。
3 每個葉節點(NIL節點,空節點)是黑色的。
4 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
5. 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
關鍵性質: 從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這個樹大致上是平衡的。因爲操作比如插入、刪除和查找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查找樹。
旋轉
旋轉是爲了讓失去平衡的紅黑樹(比如插刪操作後,左右子樹的高度不一致了。)重新恢復平衡。
如圖:
左旋,如圖所示(左->右),以x->y之間的鏈爲“支軸”進行,
使y成爲該新子樹的根,x成爲y的左孩子,而y的左孩子則成爲x的右孩子。
重新着色
旋轉後紅黑色顏色特性被破壞了。需要重新着色。
堆
某blog
也叫優先隊列,是完全二叉樹。大頂堆(小頂堆)是父節點大於(小於)兩個子節點的值,且兩個子節點也都是一個大頂堆(小頂堆)。
堆的數組表示:下標從1開始。對一個節點t,他的父節點爲t/2,左孩子節點是2t,右孩子節點2t+1.
保持堆的性質(heapfy):左右子樹都必須已經是堆(這裏,只有一個節點的樹顯然是堆)。將root 與left, right比較,最大的值與root交換,比如是right,然後遞歸調整右子樹。
數組轉堆:數組長度爲n,從堆的最後一個非葉子節點n/2的地方開始,往前進行heapfy操作。
void BuildHeap(int A[],)
{
int i;
for(i = HEAP_SIZE(A)/2; i>=1; i--)
Heapify(A, i);
}
優先隊列(priority queue)
優先隊列是一種基於堆實現的數據結構。其特性是隊列中的元素是按照從大到小排列的。
pop操作:獲取隊列中的最大值
步驟有三步,時間複雜度爲o(lg n):
1. 從堆中取堆頂元素,對應的是堆底層數據存儲結構數組A[1](注意,A[0]是不用留空的。)
2. 將數組A最後一個元素A[n]放在A[1]的位置,同時將數組長度置爲n-1.
3. 對數組從上往下往下進行heapfy操作。
push 操作:往隊列中加入一個數。
步驟有兩步,時間複雜度爲o(lg n):
1. 在數組A尾部放入這個樹。
2. 對該數組至下往上進行heapfy操作。
void Insert(int A[], int i) { //i爲插入的值
int n=++HEAP_SIZE(A);
A[n] = -99999;//小無窮
int p = n;
while(p >1 && A[PARENT(p)] < i) {
A[p] = A[PARENT(p)];
p = PARENT(p);
}
A[p]=i;
}
```
### 棧
[某文庫](http://wenku.baidu.com/link?url=2EZUKGYzfpAZoYgx7h6SCHNqmBD8DOqHluQbtiVATV4ONrdvBKgQhLBenGjvrMhpSTDbcU-HubODxTKbTwdwC04kTekr0LIKdVRGcKeOq_e)
FILO。
懶得打字了。線性表的儲存罷了。
對棧的操作,感覺更像是一種特殊的約定。他們的操作更像是在線性的數組上認爲的添加限制。即加入時只能加到數組頭,移出時只能移除數組尾部。
### 另,計算機的堆區和棧區
**堆區和棧區實際上不屬於數據結構與算法的範疇了,應該是操作系統的部分。但是名字一樣在這也補一個。懶得提煉直接複製了。**
[某blog](http://blog.csdn.net/slj_win/article/details/8608436)
一、預備知識—程序的內存分配
一個由c/C++編譯的程序佔用的內存分爲以下幾個部分
1、棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。
2、堆區(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表,呵呵。
3、全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。 - 程序結束後有系統釋放
4、文字常量區—常量字符串就是放在這裏的。 程序結束後由系統釋放
5、程序代碼區—存放函數體的二進制代碼。
二、堆和棧的理論知識
2.1申請方式
stack:
由系統自動分配。 例如,聲明在函數中一個局部變量 int b; 系統自動在棧中爲b開闢空間
heap:
需要程序員自己申請,並指明大小,在c中malloc函數
如p1 = (char *)malloc(10);
在C++中用new運算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在棧中的。
2.2
申請後系統的響應
棧:只要棧的剩餘空間大於所申請空間,系統將爲程序提供內存,否則將報異常提示棧溢出。
堆:首先應該知道操作系統有一個記錄空閒內存地址的鏈表,當系統收到程序的申請時,
會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒鏈表中。
2.3申請大小的限制
棧:在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因 此,能從棧獲得的空間較小。
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閒內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
2.4申請效率的比較:
棧由系統自動分配,速度較快。但程序員是無法控制的。
堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是直接在進程的地址空間中保留一快內存,雖然用起來最不方便。但是速度快,也最靈活。
2.5堆和棧中的存儲內容
棧: 在函數調用時,第一個進棧的是主函數中後的下一條指令(函數調用語句的下一條可執行語句)的地址,然後是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然後是函數中的局部變量。注意靜態變量是不入棧的。
當本次函數調用結束後,局部變量先出棧,然後是參數,最後棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。
## 排序算法
### 插入
思路:在一個有序的序列中,插入一個新的數,讓這個序列依然有序。
插入排序是穩定的排序,即在一個序列中,如果有兩個數相等,那麼他們之間的前後順序在排序後不變。
複雜度: 一般情況o(n2),最優情況排好序的情況o(n),最差情況o(n2),數列爲逆序。
算法:
![pic](http://img.my.csdn.net/uploads/201301/03/1357220939_7742.png)
![遞歸版](http://img.my.csdn.net/uploads/201301/10/1357799464_6962.GIF)
### 冒泡
思想:通過兩兩交換,像水中的泡泡一樣,小的先冒出來,大的後冒出來。是穩定排序。
複雜度:o(n2),沒有最優或最差情況。
算法:
![pic](http://img.my.csdn.net/uploads/201301/03/1357220942_6357.png)
### 選擇
思路:在數列中,每次選擇一個最小的值,放到新排列數列的最後面。
不穩定排序。
複雜度:o(n2)
算法:
![pic](http://img.my.csdn.net/uploads/201301/10/1357793648_7622.GIF)
### 歸併
思路:分治思想解決排序。
是穩定排序。
複雜度:o(n lgn)。
僞代碼:
mergeSort(A,i,j){
if i < j
mid = (i+j)/2
mergeSort(A,i,mid)
mergeSort(A, mid+1,j)
merge(A,i,m,g)
}
其中,merge的操作就是將兩個基本有序的數列合併到一起。這個過程需要額外的一個數組來做臨時變量。
算法:
![pic](http://img.my.csdn.net/uploads/201301/03/1357220944_1498.png)
### 快速
**基本是必考的排序。**
思路:利用分治的思想,將大問題化爲兩個小問題,然後再合併。首先在一個已排序的序列中,隨便選擇一個數。然後將比這個數大的放在這個數右邊,小的放在左邊。然後對這兩個子區間再排序。
是不穩定的排序。
複雜度:o(nlgn) 沒有最優情況,最差情況是每次選擇都選擇到了數組的最值,這個時候分區最不平衡。一般的實現上來講,會發生在排序的數列是逆序或者所有數列都是相等的時候。
解決方法是三值取中法。
算法:
![pic](http://img.my.csdn.net/uploads/201301/03/1357220950_4864.png)
### 堆
思路:利用最大堆和最小堆進行排序。將數組整理爲堆,然後每次彈出堆頂即可。
複雜度:o(nlg n)
![pic](http://img.my.csdn.net/uploads/201301/03/1357220958_2074.png)
### 基底
思路:從個位到高位,對於每一位進行排序,而每一位的排序,都視爲計數排序。
是穩定的排序,需要額外的空間。
例圖:
![pic](http://img.my.csdn.net/uploads/201301/03/1357220964_6225.png)
複雜度:o((n+k)d)
算法:
![pic](http://img.my.csdn.net/uploads/201301/03/1357220967_7660.png)
### 桶
思路:先預篩選再進行排序。把一個大的複雜的數列先按大小範圍把不同元素放到不同的桶裏面。然後對不同桶裏面的數進行排序。
穩定排序,需要額外的空間。
複雜度:最優情況是o(n),這個時候所有的元素均勻分佈到各個桶裏面,而且桶內元素正好有序。最壞情況是全部元素都分到一個桶裏面,爲o(n2)或者o(nlg n)如果桶內排序用的是快排的話。
算法:
![pic](http://img.my.csdn.net/uploads/201301/03/1357220972_2232.png)
### 計數
思路:利用數組的下標進行統計的排序。這個算法速度快(o(n),基本上這種有一定使用條件的排序算法速度上都會優於通用的排序方法。),需要額外的空間,不適用於有負數的排序和數組中含有特別大的數的數列。
複雜度:o(n+k),k爲數列中最大的數。需要額外的空間複雜度。o(k)
是不穩定的排序。本質上是先統計在根據統計還原。和原來的數組已經沒有關係了。
算法:
![pic](http://img.my.csdn.net/uploads/201301/03/1357220962_8346.png)
### ~~睡眠~~
~~這是來搞笑的。= =~~
~~構造n個線程,它們和這n個數一一對應。初始化後,線程們開始睡眠,等到對應的數那麼多個時間單位後各自醒來,然後輸出它對應的數。這樣最小的數對應的線程最早醒來,這個數最早被輸出。~~
## 算法補遺
[查找算法](http://blog.csdn.net/chinabhlt/article/details/47420391)
### 二分查找
思路:折辦查找,元素必須是有序的纔可以。用給定值k先與中間結點的關鍵字比較,中間結點把線形表分成兩個子表,若相等則查找成功;若不相等,再根據k與該中間結點關鍵字的比較結果確定下一步查找哪個子表,這樣遞歸進行,直到查找到或查找結束髮現表中沒有這樣的結點。
複雜度: o(lg n)
算法:
```c++
//二分查找(折半查找),版本1
int BinarySearch1(int a[], int value, int n)
{
int low, high, mid;
low = 0;
high = n-1;
while(low<=high)
{
mid = (low+high)/2;
if(a[mid]==value)
return mid;
if(a[mid]>value)
high = mid-1;
if(a[mid]<value)
low = mid+1;
}
return -1;
}
//二分查找,遞歸版本
int BinarySearch2(int a[], int value, int low, int high)
{
int mid = low+(high-low)/2;//這種求mid的寫法是爲了防止溢出,因爲high+low是有可能溢出的。
if(a[mid]==value)
return mid;
if(a[mid]>value)
return BinarySearch2(a, value, low, mid-1);
if(a[mid]<value)
return BinarySearch2(a, value, mid+1, high);
}
<div class="se-preview-section-delimiter"></div>
partition中位數
這個算法是在亂序的數組中查找中位數的算法。
利用快排關鍵字的查找方法
a.隨機選取一個關鍵字key,將序列二分;
b.若關鍵字的下標大於N/2,則繼續對序列的左半部分執行partition;
c.若關鍵字的下標小於N/2,則繼續對序列的右半部分執行partition;
d.若關鍵字的下標等於N/2,則返回key。
算法複雜度是o(nlg n)?.
基於流的中位數
提煉一下就是利用大小堆來存儲這個流的數。在存儲的過程中,大堆保存中位數左邊的數,小堆保存中位數右邊的數。在拿到流中的數時,動態調整(在新到達的數,大堆頂,小堆頂三個數中比較,然後選則將新的數送到哪個堆中)。需要取中位數數時只要取兩個堆頂即可。
給一個數據流,找出中位數,由於數據流中的數據並不是有序的,所以我們首先應該想個方法讓其有序。如果我們用vector來保存數據流的話,每進來一個新數據都要給數組排序,很不高效。所以之後想到用multiset這個數據結構,是有序保存數據的,但是它不能用下標直接訪問元素,找中位數也不高效。這裏用到的解法十分巧妙,我們使用大小堆來解決問題,其中大堆保存右半段較大的數字,小堆保存左半段較小的數組。這樣整個數組就被中間分爲兩段了,由於堆的保存方式是由大到小,我們希望大堆裏面的數據是從小到大,這樣取第一個來計算中位數方便。我們用到一個小技巧,就是存到大堆裏的數先取反再存,這樣由大到小存下來的順序就是實際上我們想要的從小到大的順序。當大堆和小堆中的數字一樣多時,我們取出大堆小堆的首元素求平均值,當小堆元素多時,取小堆首元素爲中位數
最短路徑
迪傑斯特拉
動態規劃
不知道該怎麼講,丟個連接dp
蓄水池抽樣
介紹
從一個長度未知或者很長的序列中隨機抽取出k個元素,保證k個元素的輸出是完全隨機的。
構造一個可以放置k個元素的蓄水池,將序列的前k個元素放入蓄水池類,然後從第k+1個元素開始,以k/n的概率來決定鈣元素是否需要被替換到水池中。
實現
僞代碼如下:
Init : a reservoir with the size: k
for i= k+1 to N
M=random(1, i);
if( M < k)
SWAP the Mth value and ith value
end for
<div class="se-preview-section-delimiter"></div>
證明
- 對於第i個數(i