數據結構知識點
1、 樹
1.1 平衡二叉樹(AVL樹)
是一種特殊的二叉排序樹。其左右子樹都是平衡二叉樹,且左右子樹高度之差的絕對值不超過1。一句話表述爲:以樹中所有結點爲根的樹的左右子樹高度之差的絕對值不超過1。平衡因子BF(Balance Factor)定義爲該節點的左子樹深度減去右子樹深度,則平衡二叉樹所有結點的平衡因子只能是-1,0,1。只要有一個結點的平衡因子絕對值大於一就是不平衡的;
1.1.1 調整平衡二叉樹
插入節點失衡調整
在插入新結點的過程中,會出現平衡因子絕對值大於2的情況,這時就需要進行一定的條件,讓二叉樹保持平衡。失去平衡後進行的調整規律可以歸納爲以下四種
- 單項右旋處理
- 單項左旋處理
- 雙向(先左後右)旋轉處理
- 雙向(先右後左)旋轉處理
左旋/右旋:
針對上面4中情況的分別處理:
1、左子樹的左節點(右旋即可)
2、右子樹的右節點(左旋即可)
3、左子樹的右節點(先左旋再右旋)
4、右子樹的左節點(先右旋再左旋)
刪除節點失衡調整
(1)當刪除的節點是葉子節點,則將節點刪除,然後從父節點開始,判斷是否失衡,如果沒有失衡,則再判斷父節點的父節點是否失衡,直到根節點,此時到根節點還發現沒有失衡,則說此時樹是平衡的;如果中間過程發現失衡,則判斷屬於哪種類型的失衡(左左,左右,右左,右右),然後進行調整。
(2)刪除的節點只有左子樹或只有右子樹,這種情況其實就比刪除葉子節點的步驟多一步,就是將節點刪除,然後把僅有一支的左子樹或右子樹替代原有結點的位置,後面的步驟就一樣了,從父節點開始,判斷是否失衡,如果沒有失衡,則再判斷父節點的父節點是否失衡,直到根節點,如果中間過程發現失衡,則根據失衡的類型進行調整。
(3)刪除的節點既有左子樹又有右子樹,這種情況又比上面這種多一步,就是中序遍歷,找到待刪除節點的前驅或者後驅都行,然後與待刪除節點互換位置,然後把待刪除的節點刪掉,後面的步驟也是一樣,判斷是否失衡,然後根據失衡類型進行調整。
代碼實現
https://www.cnblogs.com/mr-stn/p/9058567.html
1.2 紅黑樹
紅黑樹是一種二叉查找樹,但在每個節點增加一個存儲位表示節點的顏色,可以是紅或黑(非紅即黑)。通過對任何一條從根到葉子的路徑上各個節點着色的方式的限制,紅黑樹確保沒有一條路徑會比其它路徑長出兩倍,因此,紅黑樹是一種弱平衡二叉樹,相對於要求嚴格的AVL樹來說,它的旋轉次數少,插入最多兩次旋轉,刪除最多三次旋轉。在查找,插入刪除的性能都是O(logn),且性能穩定,所以STL裏面很多結構包括map底層實現都是使用的紅黑樹。
性質:
- 每個節點非紅即黑
- 根節點是黑的;
- 每個葉節點(葉節點即樹尾端NULL指針或NULL節點)都是黑的;
- 如果一個節點是紅色的,則它的子節點必須是黑色的。
- 對於任意節點而言,其到葉子點樹NULL指針的每條路徑都包含相同數目的黑節點
區別:
AVL 樹是高度平衡的,頻繁的插入和刪除,會引起頻繁的rebalance,導致效率下降;紅黑樹不是高度平衡的,算是一種折中,插入最多兩次旋轉,刪除最多三次旋轉。
1.3 B樹/B+樹/B-樹
參考https://www.jianshu.com/p/332caf8bed3a
1.3.1 B樹
B樹
即二叉搜索樹:
1.所有非葉子結點至多擁有兩個兒子(Left和Right);
2.所有結點存儲一個關鍵字;
3.非葉子結點的左指針指向小於其關鍵字的子樹,右指針指向大於其關鍵字的子樹;
1.3.2 B-樹
是一種多路搜索樹(並不是二叉的):
1.定義任意非葉子結點最多隻有M個兒子;且M>2;
2.根結點的兒子數爲[2, M];
3.除根結點以外的非葉子結點的兒子數爲[M/2, M];
4.每個結點存放至少M/2-1(取上整)和至多M-1個關鍵字;(至少2個關鍵字)
5.非葉子結點的關鍵字個數=指向兒子的指針個數-1;
6.非葉子結點的關鍵字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7.非葉子結點的指針:P[1], P[2], …, P[M];其中P[1]指向關鍵字小於K[1]的
子樹,P[M]指向關鍵字大於K[M-1]的子樹,其它P[i]指向關鍵字屬於(K[i-1], K[i])的子樹;
8.所有葉子結點位於同一層;
B-樹的特性:
1.關鍵字集合分佈在整顆樹中;
2.任何一個關鍵字出現且只出現在一個結點中;
3.搜索有可能在非葉子結點結束;
4.其搜索性能等價於在關鍵字全集內做一次二分查找;
5.自動層次控制;
1.3.3 B+樹
B+的搜索與B-樹也基本相同,區別是B+樹只有達到葉子結點才命中(B-樹可以在非葉子結點命中),其性能也等價於在關鍵字全集做一次二分查找,B+樹上有兩個頭指針,一個指向根節點,另一個指向關鍵字最小的葉子節點。因此可以對B+樹進行兩種查找運算:一種是從最小關鍵字起順序查找,另一種是從根節點開始,進行隨機查找。 ;
B+的特性:
1.所有關鍵字都出現在葉子結點的鏈表中(稠密索引),且鏈表中的關鍵字恰好是有序的;
2.不可能在非葉子結點命中;
3.非葉子結點相當於是葉子結點的索引(稀疏索引),葉子結點相當於是存儲(關鍵字)數據的數據層;
4.比B-樹更適合文件索引,數據庫索引;
爲何B+比B-更適合文件系統和數據庫索引?
- B+tree的磁盤讀寫代價更低:B+tree的內部結點並沒有指向關鍵字具體信息的指針,因此其內部結點相對B 樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多。一次性讀入內存中的需要查找的關鍵字也就越多,相對來說IO讀寫次數也就降低了;
- B+tree的查詢效率更加穩定:由於內部結點並不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引,所以,任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當;
- 數據庫索引採用B+樹而不是B-樹的主要原因:B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷,而且在數據庫中基於範圍的查詢是非常頻繁的,而B-樹只能中序遍歷所有節點,效率太低。
B+是一種多路搜索樹,主要爲磁盤或其他直接存取輔助設備而設計的一種平衡查找樹,在B+樹中,每個節點的可以有多個孩子,並且按照關鍵字大小有序排列。所有記錄節點都是按照鍵值的大小順序存放在同一層的葉節點中。相比B樹,其具有以下幾個特點:
- 每個節點上的指針上限爲2d而不是2d+1(d爲節點的出度)
- 內節點不存儲data,只存儲key
- 葉子節點不存儲指針
1.3.4 B*樹
是B+樹的變體,在B+樹的非根和非葉子結點再增加指向兄弟的指針;
B*樹的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那麼將一部分數據移到兄弟結點中,再在原結點插入關鍵字,最後修改父結點中兄弟結點的關鍵字(因爲兄弟結點的關鍵字範圍改變了);如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,並各複製1/3的數據到新結點,最後在父結點增加新結點的指針;所以,B*樹分配新結點的概率比B+樹要低,空間使用率更高;
2 哈夫曼樹
哈夫曼樹它是最優二叉樹。
定義:給定n個權值作爲n個葉子結點,構造一棵二叉樹,若樹的帶權路徑長度達到最小,則這棵樹被稱爲哈夫曼樹。
二叉樹結點的度只有兩種,一種是度爲0的葉子節點,另一種則是度爲2的內部結點,不存在度爲1 的結點
根據二叉樹的性質:度爲0的結點和度爲2 的結點的關係:n0=n2+1
很容易算出:哈夫曼樹的總結點數爲:2n0-1 (n0代表葉子節點個數)
2.1 哈夫曼樹的構造
假設有n個權值,則構造出的哈夫曼樹有n個葉子結點。 n個權值分別設爲 w1、w2、…、wn,哈夫曼樹的構造規則爲:
- 將w1、w2、…,wn看成是有n 棵樹的森林(每棵樹僅有一個結點);
- 在森林中選出根結點的權值最小的兩棵樹進行合併,作爲一棵新樹的左、右子樹,且新樹的根結點權值爲其左、右子樹根結點權值之和;
- 從森林中刪除選取的兩棵樹,並將新樹加入森林;
- 重複(02)、(03)步,直到森林中只剩一棵樹爲止,該樹即爲所求得的哈夫曼樹。
實現:
//哈夫曼樹的結點結構
struct element
{
int weight; //權值
int lchild, rchild, parent; //該結點的左、右、雙親結點在數組中的下標
};
/*
1、數組haftree初始化,所有數組元素的雙親、左右孩子都置爲-1;
2、數組haftree的前n個元素的權值置給定權值;
3、進行n-1次合併
3.1 在二叉樹集合中選取兩個權值最小的根節點,其下標分別爲i1,i2;
3.2 將二叉樹i1、i2合併爲一棵新的二叉樹k。
*/
//選取權值最小的兩個結點
void selectMin(element a[], int n, int &s1, int &s2) {
for (int i = 0; i < n; i++) {
if (a[i].parent == -1) {// 初始化s1, s1的雙親爲-1
s1 = i;
break;
}
}
// s1爲權值最小的下標
for (int i = 0; i < n; i++) {
if (a[i].parent == -1 && a[s1].weight > a[i].weight)
s1 = i;
}
for (int j = 0; j < n; j++) {
if (a[j].parent == -1 && j != s1) {// 初始化s2,s2的雙親爲-1
s2 = j;
break;
}
}
// s2爲權值次小的下標
for (int j = 0; j < n; j++) {
if (a[j].parent == -1 && a[s2].weight > a[j].weight && j != s1) {
s2 = j;
}
}
}
/*
哈夫曼算法
n個葉子結點的權值保存在數組w中
*/
void HuffmanTree(element huftree[], int w[], int n) {
// 初始化,所有結點均沒有雙親和孩子
for (int i = 0; i < 2 * n - 1; i++) { //2n-1爲總的結點個數,n爲葉子結點數
huftree[i].parent = -1;
huftree[i].lchild = -1;
huftree[i].rchild = -1;
}
// 構造只有根節點的n棵二叉樹
for (int i = 0; i < n; i++)
huftree[i].weight = w[i];
// n-1次合併
for (int k = n; k < 2 * n - 1; k++) {
int i1, i2;
selectMin(huftree, k, i1, i2);// 查找權值最小的倆個根節點,下標爲i1,i2
// 將i1,i2合併,且i1和i2的雙親爲k
huftree[i1].parent = k;
huftree[i2].parent = k;
huftree[k].lchild = i1;
huftree[k].rchild = i2;
huftree[k].weight = huftree[i1].weight + huftree[i2].weight;
}
}
void print(element h[], int n) {
cout << "index weight parent lchild rchild" << endl;
cout << left;
for (int i = 0; i < n; i++) {
cout << setw(5) << i << " ";
cout << setw(6) << h[i].weight << " ";
cout << setw(6) << h[i].parent << " ";
cout << setw(6) << h[i].lchild << " ";
cout << setw(6) << h[i].rchild << endl;
}
}
int main()
{
int x[] = { 5, 29, 7, 8, 14, 23, 3, 11 };//葉子結點權值集合
element* hufftree = new element[2*8-1];//動態創建數組
HuffmanTree(hufftree, x, 8);
print(hufftree, 15);//合併新增的非葉子結點
system("pause");
return 0;
}
哈夫曼編碼是哈夫曼樹的一種應用,廣泛用於數據文件壓縮。哈夫曼編碼算法用字符在文件中出現的頻率來建立使用0,1表示個字符的最優表示方式,其具體算法如下:
(1)哈夫曼算法以自底向上的方式構造表示最優前綴碼的二叉樹T。
(2)算法以|C|個葉結點開始,執行|C|-1次的“合併”運算後產生最終所要求的樹T。
(3)假設編碼字符集中每一字符c的頻率是f©。以f爲鍵值的優先隊列Q用在貪心選擇時有效地確定算法當前要合併的2棵具有最小頻率的樹。一旦2棵具有最小頻率的樹合併後,產生一棵新的樹,其頻率爲合併的2棵樹的頻率之和,並將新樹插入優先隊列Q。經過n-1次的合併後,優先隊列中只剩下一棵樹,即所要求的樹T。
3 map和unordered_map優點和缺點
對於map,其底層是基於紅黑樹實現的,優點如下:
1)有序性,這是map結構最大的優點,其元素的有序性在很多應用中都會簡化很多的操作
2)map的查找、刪除、增加等一系列操作時間複雜度穩定,都爲logn
map缺點如下:
1)查找、刪除、增加等操作平均時間複雜度較慢,與n相關
對於unordered_map來說,其底層是一個哈希表,優點如下:
查找、刪除、添加的速度快,時間複雜度爲常數級O( c)
unordered_map缺點如下:
因爲unordered_map內部基於哈希表,以(key,value)對的形式存儲,因此空間佔用率高
Unordered_map的查找、刪除、添加的時間複雜度不穩定,平均爲O( c),取決於哈希函數。極端情況下可能爲O(n)
4 Top(K)問題
1、直接全部排序(只適用於內存夠的情況)
當數據量較小的情況下,內存中可以容納所有數據。則最簡單也是最容易想到的方法是將數據全部排序,然後取排序後的數據中的前K個。
2、快速排序的變形 (只使用於內存夠的情況)
這是一個基於快速排序的變形,首先選擇一個劃分元,將比這個劃分元大的元素放到它的前面,比劃分元小的元素放到它的後面,此時完成了一趟排序。如果此時這個劃分元的序號index剛好等於K,那麼這個劃分元以及它左邊的數,剛好就是前K個最大的元素;如果index > K,那麼前K大的數據在index的左邊,那麼就繼續遞歸的從index-1個數中進行一趟排序;如果index < K,那麼再從劃分元的右邊繼續進行排序,直到找到序號index剛好等於K爲止。再將前K個數進行排序後,返回Top K個元素。這種方法就避免了對除了Top K個元素以外的數據進行排序所帶來的不必要的開銷。
3、最小堆法
這是一種局部淘汰法。先讀取前K個數,建立一個最小堆。然後將剩餘的所有數字依次與最小堆的堆頂進行比較,如果小於或等於堆頂數據,則繼續比較下一個;否則,刪除堆頂元素,並將新數據插入堆中,重新調整最小堆。當遍歷完全部數據後,最小堆中的數據即爲最大的K個數。
4、分治法
將全部數據分成N份,前提是每份的數據都可以讀到內存中進行處理,找到每份數據中最大的K個數。此時剩下N*K個數據,如果內存不能容納N*K個數據,則再繼續分治處理,分成M份,找出每份數據中最大的K個數,如果M*K個數仍然不能讀到內存中,則繼續分治處理。直到剩餘的數可以讀入內存中,那麼可以對這些數使用快速排序的變形或者歸併排序進行處理。
5、Hash法
如果這些數據中有很多重複的數據,可以先通過hash法,把重複的數去掉。這樣如果重複率很高的話,會減少很大的內存用量,從而縮小運算空間。處理後的數據如果能夠讀入內存,則可以直接排序;否則可以使用分治法或者最小堆法來處理數據。
5 二叉樹的層序遍歷並輸出
//主要利用隊列輔助實現
void layerTree(BTreeNode* T)
{
if (T == NULL)
return;
BTreeNode* p = T;
queue<BTreeNode*> que;
que.push(p);
while (!que.empty())
{
p = que.front();
que.pop();
cout << p->val;
if (p->left) que.push(p->left);
if (p->right) que.push(p->right);
}
}
6 由中序和前序恢復二叉樹
//由中序和前序恢復二叉樹
BTreeNode* helper(vector<int> pre, int pre_start, int pre_end, vector<int> vin, int vin_start, int vin_end)
{
if (pre_start > pre_end || vin_start > vin_end)
return nullptr;
BTreeNode* root = new BTreeNode(pre[pre_start]);
for (int i = vin_start; i <= vin_end; i++) {
if (vin[i] == pre[pre_start]) {
root->left = helper(pre, pre_start + 1, pre_start + i - vin_start, vin, vin_start, i - 1);
root->right = helper(pre, pre_start + i - vin_start + 1, pre_end, vin, i + 1, vin_end);
break;
}
}
return root;
}
BTreeNode* reConstructBTree(vector<int> pre, vector<int> vin)
{
BTreeNode* root = helper(pre, 0, pre.size() - 1, vin, 0, vin.size() - 1);
return root;
}
7 兩個棧實現一個隊列
//兩個棧實現一個隊列
//1、入隊列:壓棧stack1
//2、出隊列:stack2棧頂應該爲出隊元素。若stack2爲空,則將stack1逐個彈出壓入stack2,stack2的棧頂就是出隊元素;
class solution {
public:
void push(int node) {
stack1.push(node);
}
int pop() {
if (stack2.size() != 0) {
int temp = stack2.top();
stack2.pop();
return temp;
}
else {
while (stack1.size() != 0)
{
int tmp = stack1.top();
stack1.pop();
stack2.push(tmp);
}
}
return pop();
}
private:
stack<int> stack1;
stack<int> stack2;
};
8 判斷該數組否有重複的數
//長度爲N的整形數組,數組中每個元素的取值範圍是[0,n-1],判斷該數組否有重複的數
bool IsDupNumber(int* arr, int n) {
if (arr == NULL)
return false;
int temp;
for (int i = 0; i < n; i++) {
while (arr[i] != i)
{
if (arr[arr[i]] == arr[i])
return true;
temp = arr[arr[i]];
arr[arr[i]] = arr[i];
arr[i] = temp;
}
}
return false;
}
9 排序算法
順序查找的時間複雜度爲o(n)
分塊查找的時間複雜度爲o(log2n)到o(n)之間
二分查找的時間複雜度爲o(log2n)
哈希查找的時間複雜度爲o(1)
9.1 快速排序
//通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,
//然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
void QuickSort(int* arr, int start, int end) {
int temp = arr[start];
int i = start;
int j = end;
if (i < j) { //遞歸結束條件
while (i<j)
{
//從右往左找比基準小的值
while (i<j && arr[j] > temp) j--;
if (i < j) arr[i++] = arr[j];
//從左往右找比基準大的值
while (i < j && arr[i] < temp) i++;
if (i < j) arr[j--] = arr[i];
}
//把基準數放到適當的位置
arr[i] = temp;
//遞歸
QuickSort(arr, start, i - 1);//左半部分
QuickSort(arr, i + 1, end);//右半部分
}
}
9.2 冒泡排序
//交換函數
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
//冒泡排序,從小到大
void BubbleSort(int *pData, int count)
{
int flag = 0;
for (int i = 1; i < count && flag == 0; i++) {
flag = 1;
for (int j = count - 1; j >= i; j--)
if (pData[j] < pData[j - 1]) {
flag = 0;
swap(&pData[j], &pData[j - 1]);
}
}
}
9.3 直接插入排序
//直接插入排序,從小到大
//對於一個帶排序數組來說,其初始有序數組元素個數爲1,然後從第二個元素,插入到有序數組中。對於每一次插入操作,從後往前遍歷當前有序數組,如果當前元素大於要插入的元素,則後移一位;
//如果當前元素小於或等於要插入的元素,則將要插入的元素插入到當前元素的下一位中。
void InsertSort(int* arr, int count) {
for (int i = 1; i < count; i++) {
int itemp = arr[i];
int ipos = i-1;
while (ipos >= 0 && arr[ipos] > itemp)
{
arr[ipos + 1] = arr[ipos];//大的值後移
ipos--;
}
//插入位置
arr[ipos + 1] = itemp;
}
}
9.4 歸併排序
//該算法採用分治法;對於包含m個元素的待排序序列,將其看成m個長度爲1的子序列。
//然後兩兩合歸併,得到n/2個長度爲2或者1的有序子序列;然後再兩兩歸併,直到得到1個長度爲m的有序序列。
void Merge(int* arr, int start, int end, int mid, int* temp) {
//左半邊
int i_start = start;
int i_end = mid;
//右半部
int j_start = mid + 1;
int j_end = end;
int k = 0;//輔助空間
//合併兩個有序序列
while (i_start <= i_end && j_start <= j_end)
{
if (arr[i_start] < arr[j_start])
temp[k++] = arr[i_start++];
else
temp[k++] = arr[j_start++];
}
//剩下的元素
while (i_start<=i_end)
temp[k++] = arr[i_start++];
while (j_start<=j_end)
temp[k++] = arr[j_start++];
//輔助空間覆蓋原空間
for (int i = 0; i < k; i++)
arr[start + i] = temp[i];//注意此處的start+i
}
//歸併排序
void MergeSort(int* arr, int start, int end, int* temp) {
if (start >= end)
return;
int mid = (end + start) / 2;
//左半邊
MergeSort(arr, start, mid, temp);
//右半部
MergeSort(arr, mid + 1, end, temp);
//合併
Merge(arr, start, end, mid, temp);
}
9.5 希爾排序
//先將整個待排序記錄分割成若干子序列,然後分別進行直接插入排序,待整個序列中的記錄基本有序時,在對全體記錄進行一次直接插入排序。
//其子序列的構成不是簡單的逐段分割,而是將每隔某個增量的記錄組成一個子序列。希爾排序時間複雜度與增量序列的選取有關,其最後一個值必須爲1.
void ShellSort(int* arr, int length) {
//步長
int increase = length;
int i, j, k;
do {
//確定分組增量(邏輯分組)
increase = increase / 3 + 1;
for (i = 0; i < increase; i++) {
for (j = i + increase; j < length; j += increase) {
if (arr[j] < arr[j - increase]) {
int temp = arr[j];
for (k = j - increase; k >= 0 && temp < arr[k]; k -= increase) {
arr[k + increase] = arr[k];
}
arr[k + increase] = temp;
}
}
}
} while (increase > 1);
}
9.6 選擇排序
//減少交換次數
//每次循環,選擇當前無序數組中最小的那個元素,然後將其與無序數組的第一個元素交換位置,從而使有序數組元素加1,無序數組元素減1。
void SelectSort(int* arr, int count) {
int min = 0;
for (int i = 0; i < count-1; i++) {
min = i;
for (int j = i + 1; j < count; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
if (min != i) {
swap(&arr[i], &arr[min]);
}
}
}
9.7 堆排序
//堆排序是一種選擇排序,利用堆這種數據結構來完成選擇。其算法思想是將帶排序數據構造一個最大堆(升序)/最小堆(降序),
//然後將堆頂元素與待排序數組的最後一個元素交換位置,此時末尾元素就是最大/最小的值。然後將剩餘n-1個元素重新構造成最大堆/最小堆。
void MySwap(int arr[], int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
//堆調整
/*
arr 待調整的數組; index 待調整的結點的下標;len 數組長度
*/
void HeapAdjust(int arr[], int index, int len) {
int max = index;
int lchild = index * 2 + 1;
int rchild = lchild + 1;
if (lchild < len && arr[lchild] > arr[max])
max = lchild;
if (rchild < len && arr[rchild] > arr[max])
max = rchild;
if (max != index) {
MySwap(arr, index, max);
HeapAdjust(arr, max, len);//注意要繼續調整index=max的子節點
}
}
//堆排序
void HeapSort(int arr[], int len) {
for (int i = len / 2 - 1; i >= 0; i--)
HeapAdjust(arr, i, len);//從最後一個葉子節點的父節點開始
//交換堆頂元素和最後一個
for (int i = len - 1; i >= 0; i--) {
MySwap(arr, 0, i);
HeapAdjust(arr, 0, i);//從頭往下調整
}
cout << arr[0] << endl;
}
10 hash表的實現
哈希表,也稱散列表,是實現字典操作的一種有效的數據結構(key,value)。
10.1 常用的哈希函數
1、除留餘數法:
H(Key) = key % p (p ≤ m)
#p最好選擇一個小於或等於m(哈希地址集合的個數)的某個最大素數
2、直接地址法
H(Key) = a * Key + b;這個“a,b”是常量。
3、數字分析法
比如有一組key1=112233,key2=112633,key3=119033,
針對這樣的數我們分析數中間兩個數比較波動,其他數不變。那麼我們取key的值就可以是 key1=22,key2=26,key3=90。
10.2 衝突處理方法
當兩個不同的數據元素的哈希值相同時,就會發生衝突。
10.2.1 開放地址法
開放地址法有個非常關鍵的特徵,就是所有輸入的元素全部存放在哈希表裏,也就是說,位桶的實現是不需要任何的鏈表來實現的,換句話說,也就是這個哈希表的裝載因子不會超過1。它的實現是在插入一個元素的時候,先通過哈希函數進行判斷,若是發生哈希衝突,就以當前地址爲基準,根據再尋址的方法(探查序列),去尋找下一個地址,若發生衝突再去尋找,直至找到一個爲空的地址爲止。所以這種方法又稱爲再散列法。有幾種常用的探查序列的方法:
1、線性探測法
依次探測下一個地址,直到有空的地址後插入,若整個空間都找遍仍然找不到空餘的地址,產生溢出。
2、二次探測法
地址增量序列爲:
3、雙哈希函數探測法
H(Key) , RH(Key) 是兩個哈希函數,m爲哈希表長度。先用第一個哈希函數對關鍵字計算哈希地址,一旦產生地址衝突,再用第二個函數確定移動的步長因子,最後通過步長因子序列由探測函數尋找空餘的哈希地址。
10.2.2 鏈地址法
將哈希值相同的數據元素存放在一個鏈表中,在查找哈希表的過程中,當查找到這個鏈表時,必須採用線性查找方法
紫色部分即代表哈希表,也稱爲哈希數組,數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決衝突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中,即鏈接在桶後。
10.2.3 公共溢出區
建立一個公共溢出區域,把hash衝突的元素都放在該溢出區裏。查找時,如果發現hash表中對應桶裏存在其他元素,還需要在公共溢出區裏再次進行查找。
10.3 哈希表的桶個數爲什麼是質數,合數有何不妥?
哈希表的桶個數使用質數,可以最大程度減少衝突概率,使哈希後的數據分佈的更加均勻。如果使用合數,可能會造成很多數據分佈會集中在某些點上,從而影響哈希表效率。
11 動態規劃
通過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。動態規劃常常適用於有重疊子問題和最優子結構性質的問題。
主要參考:https://www.cnblogs.com/raichen/p/5772056.html
步驟
- 描述最優解的結構
- 遞歸定義最優解的值
- 按自底向上的方式計算最優解的值
- 由計算出的結果構造一個最優解
11.1 揹包問題
有N件物品和一個容量爲V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
f[i][v]表示前i件物品恰放入一個容量爲v的揹包可以獲得的最大價值。則其狀態轉移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。
將前i件物品放入容量爲v的揹包中”這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化爲一個只牽扯前i-1件物品的問題。如果不放第i件物品,那麼問題就轉化爲“前i-1件物品放入容量爲v的揹包中”;如果放第i件物品,那麼問題就轉化爲“前i-1件物品放入剩下的容量爲v-c[i]的揹包中 ”,此時能獲得的最大價值就是f [i-1][v-c[i]]。再加上通過放入第i件物品獲得的價值w[i]。
/*
w:物品重量 (w[0] = 0)有效值從1開始
v:物品價值 (v[0] = 0)
m:揹包容量
n:真實物品個數
返回:總價值
將哪些物品裝入揹包可使這些物品的重量總和不超過揹包容量,且價值總和最大。
*/
int package_dp(vector<int>& w, vector<int>& v, int m, int n) {
vector< vector<int> > vec(n+1, vector<int>(m+1, 0));//vec[n+1][m+1]
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (w[i] > j)
vec[i][j] = vec[i - 1][j];
else {
int tmp1 = v[i] + vec[i - 1][j - w[i]];//放入i
int tmp2 = vec[i - 1][j];//不放i
vec[i][j] = tmp1 > tmp2 ? tmp1 : tmp2;
}
}
}
return vec[n][m];
}
int main()
{
vector<int> w = { 0,10,25,40,20,10 };
vector<int> v = { 0,40,50,70,40,20 };
int m = 120;
int val = package_dp(w, v, m, w.size()-1);
cout << val << endl;
system("pause");
return 0;
}
11.2 最長公共子序列(不連續)LCS
cnblogs與belong,最長公共子序列爲blog(cnblogs, belong),最長公共子串爲lo(cnblogs, belong)
解題思路:
- 窮舉法
對X的每一個子序列,檢查它是否也是Y的子序列,從而確定它是否爲X和Y的公共子序列,並且在檢查過程中選出最長的公共子序列。X和Y的所有子序列都檢查過後即可求出X和Y的最長公共子序列。
- 動態規劃算法
記:
假定
(1)若(最後一個字符相同),即有且顯然有,即Z的前綴的最長公共子序列。此時,問題化歸成求的長度等於+1)。
(2)若 ≠ ,可以看出要麼,要麼。此時,問題化歸成求:
也就是說,解決這個LCS問題,你要求三個方面的東西
狀態轉移方程:
用i,j遍歷兩個子串x,y,如果兩個元素相等就+1 ,不等就用上一個狀態最大的元素
void LCS(string str1, string str2, int len1, int len2, int c[][MAXLEN], int b[][MAXLEN]) {
int i, j;
//c[i][j]=1:代表str1中第i個元素與str2中第j個元素相同
for (i = 0; i <= len1; i++)
c[i][0] = 0;
for (j = 1; j <= len2; j++)
c[0][j] = 0;
for (i = 1; i <= len1; i++) {
for (j = 1; j <= len2; j++) {
if (str1[i - 1] == str2[j - 1]) {
c[i][j] = c[i - 1][j - 1] + 1;
b[i][j] = 0;//記錄c[i][j]是通過哪一個子問題的值求得的
}
else {
if (c[i - 1][j] >= c[i][j - 1]) {
c[i][j] = c[i - 1][j];
b[i][j] = 1;
}
else {
c[i][j] = c[i][j - 1];
b[i][j] = 2;
}
}
}
}
}
void PrintLCS(int b[][MAXLEN], string x, int i, int j) {//遞歸回溯最長子序列
if (i == 0 || j == 0)
return;
if (b[i][j] == 0) {
PrintLCS(b, x, i - 1, j - 1);
cout << x[i - 1];
}
else if (b[i][j] == 1)
PrintLCS(b, x, i - 1, j);
else
PrintLCS(b, x, i, j - 1);
}
int main()
{
string s1 = "ABCBDAB";
string s2 = "BDCABA";
int len1 = s1.length();
int len2 = s2.length();
int c[MAXLEN][MAXLEN];
int b[MAXLEN][MAXLEN];
LCS(s1, s2, len1, len2, c, b);
cout << c[len1][len2] << endl;//輸出最長子序列長度
PrintLCS(b, s1, len1, len2);
cout << endl;
system("pause");
return 0;
}
11.3 最長公共子串(連續)
狀態轉移方程:
區別就是因爲是連續的,如果兩個元素不等,那麼就要=0了而不能用之前一個狀態的最大元素
string getLCS(string s1, string s2, int len1, int len2) {
vector<vector<int>> vec(len1, vector<int>(len2));
int maxlen = 0, maxend = 0;
for (int i = 0; i < len1; i++)
for (int j = 0; j < len2; j++) {
if (s1[i] == s2[j]) {
if (i == 0 || j == 0)
vec[i][j] = 1;
else
vec[i][j] = vec[i - 1][j - 1] + 1;
}
else
vec[i][j] = 0;
if (vec[i][j] > maxlen) {
maxlen = vec[i][j];
maxend = i;
}
}
return s1.substr(maxend - maxlen + 1, maxlen);
}
11.4 KMP算法
常用於在一個文本串S內查找一個模式串P 的出現位置。
- 暴力匹配
假設現在文本串S匹配到 i 位置,模式串P匹配到 j 位置,則有:
如果當前字符匹配成功(即S[i] == P[j]),則i++,j++,繼續匹配下一個字符;
如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j 被置爲0。
- KMP算法
假設現在文本串S匹配到 i 位置,模式串P匹配到 j 位置
如果j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++,繼續匹配下一個字符;
如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]。此舉意味着失配時,模式串P相對於文本串S向右移動了j - next [j] 位。
在某個字符失配時,該字符對應的next 值會告訴你下一步匹配中,模式串應該跳到哪個位置(跳到next [j] 的位置)。如果next [j] 等於0或-1,則跳到模式串的開頭字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某個字符,而不是跳到開頭,且具體跳過了k 個字符。
下面解釋爲何跳到next[j]位置:
當匹配到該位置時,失配;尋找除過當前元素D之外的模式串中的前綴後綴最長公共元素,即就是AB
下一步不是從頭開始,而是將模式串移動到如下位置,向右移動了j - next [j] 位。
具體步驟:
- 尋找前綴後綴最長公共元素長度,如果模式串爲“abab”,那麼它的各個子串的前綴後綴的公共元素的最大長度如下:
- 求next數組
next 數組考慮的是除當前字符外的最長相同前綴後綴,所以通過第①步求得各個前綴後綴的公共元素的最大長度後,將第①步中求得的值整體右移一位,然後初值賦爲-1,對應的值即爲公共元素的下標:
vector<int> getNext(string T){
vector<int> next(T.size(), 0); // next矩陣
next[0] = -1; // next矩陣的第0位爲-1
int k = 0; // k值
for (int j = 2; j < T.size(); ++j) // 從字符串T的第2個字符開始,計算每個字符的next值
{
while (k > 0 && T[j - 1] != T[k])
k = next[k];
if (T[j - 1] == T[k])
k++;
next[j] = k;
}
return next; // 返回next矩陣
}
int KMP(string S, string T){
vector<int> next = getNext(T);
int i = 0, j = 0;
while (S[i] != '\0' && T[j] != '\0')
{
if (S[i] == T[j])
{
++i;
++j;
}
else
j = next[j];
if (j == -1)
{
++i;
++j;
}
}
if (T[j] == '\0')
return i - j;//返回下標
else
return -1;
}
11.5 硬幣找零問題
int main()
{
int n;
while (cin >> n) {
vector<int> c(n + 1, 0);
for (int i = 1; i <= n; i++) {
if (i == 1 || i == 5 || i == 10 || i == 20 || i == 50 || i == 100) {
c[i] = 1;
continue;
}
int curMin = MaxNum;
if (i - 1 > 0)
curMin = c[i - 1] < curMin ? c[i - 1] : curMin;
if (i - 5 > 0)
curMin = c[i - 5] < curMin ? c[i - 5] : curMin;
if (i - 10 > 0)
curMin = c[i - 10] < curMin ? c[i - 10] : curMin;
if (i - 20 > 0)
curMin = c[i - 20] < curMin ? c[i - 20] : curMin;
if (i - 50 > 0)
curMin = c[i - 50] < curMin ? c[i - 50] : curMin;
if (i - 100 > 0)
curMin = c[i - 100] < curMin ? c[i - 100] : curMin;
c[i] = curMin + 1;
}
cout << c[n] << endl;
}
system("pause");
return 0;
}
11.5.1 類似硬幣的問題找平方個數最小
//給一個正整數 n, 找到若干個完全平方數(比如1, 4, 9, ... )使得他們的和等於 n。你需要讓平方數的個數最少。
//給出 n = 12, 返回 3 因爲 12 = 4 + 4 + 4。
//給出 n = 13, 返回 2 因爲 13 = 4 + 9。
int findMin(int n)
{
int *result = new int(n + 1);
result[0] = 0;
for (int i = 1; i <= n; i++)
{
int minNum = i;
for (int j = 1;; j++)
{
if (i >= j * j)
{
int tmp = result[i - j * j] + 1;//匹配一個平方數
minNum = tmp < minNum ? tmp : minNum;
}
else
break;
}
result[i] = minNum;
}
return result[n];
}
int main()
{
int n;
while (cin >> n)
cout << findMin(n) << endl;
system("pause");
return 0;
}
11.6 最長迴文字串
迴文是指正着讀和倒着讀,結果一些樣,比如abcba或abba。
迴文字符串的子串也是迴文,比如P[i,j](表示以i開始以j結束的子串)是迴文字符串,那麼P[i+1,j-1]也是迴文字符串。這樣最長迴文子串就能分解成一系列子問題了。
首先定義狀態方程和轉移方程:
P[i,j]=0表示子串[i,j]不是迴文串。P[i,j]=1表示子串[i,j]是迴文串。
int LongPalindromSub(string& a) {
int len = a.length();
vector<vector<int>> dp(len, vector<int>(len, 0));
for (int i = 0; i < len; i++)
dp[i][i] = 1;
int max_len = 1;
int start_index = 0;
for (int i = len - 2; i >= 0; i--) {//i爲字串起始
for (int j = i + 1; j < len; j++) {//j爲字串結尾
if (a[i] == a[j]) {
if (j - i == 1)
dp[i][j] = 2;//相鄰元素相同則長度爲2
else {
if (j - i > 1)
dp[i][j] = dp[i + 1][j - 1] + 2;//不相鄰元素相同,長度爲P[i+1,j-1]+2
}
if (max_len < dp[i][j]) {
max_len = dp[i][j];
start_index = i;
}
}
else
dp[i][j] = 0;
}
}
string sub = a.substr(start_index, max_len);
cout << "max len is " << max_len << endl;
cout << "start index is " << start_index << endl;
cout << "substr is: " << sub << endl;
return max_len;
}
11.7 最長遞增序列
vec[i]:表示數組前i個元素中(包括第i個),最長遞增子序列的長度
vec[i] = max{ vec[i] , vec[k]+1 }, 0 <= k < i, a[i]>a[k]
vec數組的值表示前i個元素的最長子序列。i從第一個元素到最後一個元素遍歷一遍,j從第一個元素到第i個元素遍歷,如果第i個元素大於j,並且vec[J] + 1比vec[I]還大就更新,相當於把j加入到這個遞增序列了
int LIS(int* a, int length) {
vector<int> vec(length, 1);//表示數組前i個元素中(包括第i個),最長遞增子序列的長度
for (int i = 0; i < length; i++)
for (int j = 0; j < i; j++)
if (a[i] > a[j] && vec[j] + 1 > vec[i])
vec[i] = vec[j] + 1;
int max = vec[0];
for (int i = 1; i < length; i++)
if (vec[i] > max)
max = vec[i];
return max;
}
11.8 樓層拋珠問題
某幢大樓有100層。這幢大樓有個臨界樓層。低於它的樓層,往下扔玻璃珠,玻璃珠不會碎,等於或高於它的樓層,扔下玻璃珠,玻璃珠一定會碎。玻璃珠碎了就不能再扔。現在讓你設計一種方式,使得在該方式下,最壞的情況扔的次數比其他任何方式最壞的次數都少。也就是設計一種最有效的方式。
例如:有這樣一種方式,第一次選擇在60層扔,若碎了,說明臨界點在60層及以下樓層,這時只有一顆珠子,剩下的只能是從第一層,一層一層往上實驗,最壞的情況,要實驗59次,加上之前的第一次,一共60次。若沒碎,則只要從61層往上試即可,最多隻要試40次,加上之前一共需41次。兩種情況取最多的那種。故這種方式最壞的情況要試60次。仔細分析一下。如果不碎,我還有兩顆珠子,第二顆珠子會從N+1層開始試嗎?很顯然不會,此時大樓還剩100-N層,問題就轉化爲100-N的問題了。
那該如何設計方式呢?
根據題意很容易寫出狀態轉移方程:N層樓如果從n層投下玻璃珠,最壞的嘗試次數是:
那麼所有層投下的最壞嘗試次數的最小值即爲問題的解:
dp[0] = 0;
dp[1] = 1;
int floorThr(int N)
{
for (int i = 2; i <= N; i++)
{
dp[i] = i;
for (int j = 1; j < i; j++)
{
int tmp = max(j, 1 + dp[i - j]); //j的遍歷相當於把每層都試一遍
if (tmp < dp[i])
dp[i] = tmp;
}
}
return dp[N];
}
11.9 N*N方格的走法問題
//n*n方格邊的走法
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n;
while (cin >> n)
{
vector<vector<int>> dp(n+1, vector<int>(n+1, 1));
for (int i = 1; i <= n;i++)
{
for (int j = 1; j <= n;j++)
{
dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
}
}
cout << dp[n][n] << endl;
}
}
n*m方格內走法
注意不同於上面的走法
int uniquePaths(int m, int n) {
if(m<=0 || n<=0) return 0;
else if(m==1||n==1) return 1;
else if(m==2 && n==2) return 2;
else if((m==3 && n==2) || (m==2 && n==3)) return 3;
if(a[m][n]>0) return a[m][n];
a[m-1][n] = uniquePaths(m-1,n);
a[m][n-1] = uniquePaths(m,n-1);
a[m][n] = a[m-1][n]+a[m][n-1];
return a[m][n];
}