數據結構

1、 樹

1.1 平衡二叉樹(AVL樹)

是一種特殊的二叉排序樹。其左右子樹都是平衡二叉樹,且左右子樹高度之差的絕對值不超過1。一句話表述爲:以樹中所有結點爲根的樹的左右子樹高度之差的絕對值不超過1。平衡因子BF(Balance Factor)定義爲該節點的左子樹深度減去右子樹深度,則平衡二叉樹所有結點的平衡因子只能是-1,0,1。只要有一個結點的平衡因子絕對值大於一就是不平衡的;

1.1.1 調整平衡二叉樹

插入節點失衡調整

在插入新結點的過程中,會出現平衡因子絕對值大於2的情況,這時就需要進行一定的條件,讓二叉樹保持平衡。失去平衡後進行的調整規律可以歸納爲以下四種

  1. 單項右旋處理
  2. 單項左旋處理
  3. 雙向(先左後右)旋轉處理
  4. 雙向(先右後左)旋轉處理
    左旋/右旋:
    在這裏插入圖片描述在這裏插入圖片描述
    針對上面4中情況的分別處理:
    1、左子樹的左節點(右旋即可)
    在這裏插入圖片描述
    2、右子樹的右節點(左旋即可)
    在這裏插入圖片描述
    3、左子樹的右節點(先左旋再右旋)
    在這裏插入圖片描述
    4、右子樹的左節點(先右旋再左旋)
    在這裏插入圖片描述

刪除節點失衡調整

(1)當刪除的節點是葉子節點,則將節點刪除,然後從父節點開始,判斷是否失衡,如果沒有失衡,則再判斷父節點的父節點是否失衡,直到根節點,此時到根節點還發現沒有失衡,則說此時樹是平衡的;如果中間過程發現失衡,則判斷屬於哪種類型的失衡(左左,左右,右左,右右),然後進行調整。

(2)刪除的節點只有左子樹或只有右子樹,這種情況其實就比刪除葉子節點的步驟多一步,就是將節點刪除,然後把僅有一支的左子樹或右子樹替代原有結點的位置,後面的步驟就一樣了,從父節點開始,判斷是否失衡,如果沒有失衡,則再判斷父節點的父節點是否失衡,直到根節點,如果中間過程發現失衡,則根據失衡的類型進行調整。

(3)刪除的節點既有左子樹又有右子樹,這種情況又比上面這種多一步,就是中序遍歷,找到待刪除節點的前驅或者後驅都行,然後與待刪除節點互換位置,然後把待刪除的節點刪掉,後面的步驟也是一樣,判斷是否失衡,然後根據失衡類型進行調整。

代碼實現

https://www.cnblogs.com/mr-stn/p/9058567.html

1.2 紅黑樹

紅黑樹是一種二叉查找樹,但在每個節點增加一個存儲位表示節點的顏色,可以是紅或黑(非紅即黑)。通過對任何一條從根到葉子的路徑上各個節點着色的方式的限制,紅黑樹確保沒有一條路徑會比其它路徑長出兩倍,因此,紅黑樹是一種弱平衡二叉樹,相對於要求嚴格的AVL樹來說,它的旋轉次數少,插入最多兩次旋轉,刪除最多三次旋轉。在查找,插入刪除的性能都是O(logn),且性能穩定,所以STL裏面很多結構包括map底層實現都是使用的紅黑樹。
性質:

  1. 每個節點非紅即黑
  2. 根節點是黑的;
  3. 每個葉節點(葉節點即樹尾端NULL指針或NULL節點)都是黑的;
  4. 如果一個節點是紅色的,則它的子節點必須是黑色的。
  5. 對於任意節點而言,其到葉子點樹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-更適合文件系統和數據庫索引?

  1. B+tree的磁盤讀寫代價更低:B+tree的內部結點並沒有指向關鍵字具體信息的指針,因此其內部結點相對B 樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多。一次性讀入內存中的需要查找的關鍵字也就越多,相對來說IO讀寫次數也就降低了;
  2. B+tree的查詢效率更加穩定:由於內部結點並不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引,所以,任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當;
  3. 數據庫索引採用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,哈夫曼樹的構造規則爲:

  1. 將w1、w2、…,wn看成是有n 棵樹的森林(每棵樹僅有一個結點);
  2. 在森林中選出根結點的權值最小的兩棵樹進行合併,作爲一棵新樹的左、右子樹,且新樹的根結點權值爲其左、右子樹根結點權值之和;
  3. 從森林中刪除選取的兩棵樹,並將新樹加入森林;
  4. 重複(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、線性探測法
依次探測下一個地址,直到有空的地址後插入,若整個空間都找遍仍然找不到空餘的地址,產生溢出。

Hi=(H(Key)+di)H_i =( H(Key) + d_i ) % m ( i = 1,2,3,...,k , k ≤ m-1 )
di=1,2,...,m1,i地址增量 d_i = 1,2,...,m-1 , 其中 i 爲探測次數

2、二次探測法

地址增量序列爲:di=12,12,22,22,...,q2,q2(qm/2di = 1^2,-1^2,2^2,-2^2,...,q^2,-q^2(q ≤ m/2)

3、雙哈希函數探測法

Hi=(H(Key)+iRH(Key))H_i =( H(Key) + i * RH(Key) ) % m ( i = 1,2,3,..., m-1 )

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的最長公共子序列。

  • 動態規劃算法

記:

Xi=&lt;x1,...,xj&gt;Xi(1im)X_i=&lt;x_1,...,x_j&gt;X序列的前i個字符 (1≤i≤m)
Yj=&lt;y1,...,yj&gt;Yj(1jn)Y_j=&lt;y_1, ...,y_j&gt;Y序列的前j個字符 (1≤j≤n)
假定 Z=&lt;z1,z2,...,zk&gt;LCS(X,Y)Z=&lt;z_1, z_2,...,z_k&gt; \in LCS(X,Y)

(1)若xm=ynx_m=y_n(最後一個字符相同),即有zk=xm=ynz_k = x_m = y_n且顯然有Zk1LCS(Xm1,Yn1)Z_{k-1}∈LCS(X_{m-1} , Y_{n-1}),即Z的前綴Zk1Xm1Yn1Z_{k-1}是X_{m-1}與Y_{n-1}的最長公共子序列。此時,問題化歸成求Xm1Yn1LCS(LCS(X,Y)X_{m-1}與Y_{n-1}的LCS (LCS(X , Y)的長度等於LCS(Xm1,Yn1)LCS(X_{m-1} , Y_{n-1})+1)。

(2)若xmx_myny_n,可以看出要麼ZLCS(Xm1,Y)Z∈LCS(X_{m-1}, Y),要麼ZLCS(X,Yn1)Z∈LCS(X , Y_{n-1})。此時,問題化歸成求:max(LCS(Xm1,Y),LCS(X,Yn1))max(LCS(X_{m-1} , Y), LCS(X , Y_{n-1}))。

也就是說,解決這個LCS問題,你要求三個方面的東西

1LCSXm1Yn1)+11、LCS(X_{m-1},Y_{n-1})+1
2LCSXm1YLCSXYn12、LCS(X_{m-1},Y),LCS(X,Y_{n-1})
3max(LCSXm1YLCSXYn1))3、max(LCS(X_{m-1},Y),LCS(X,Y_{n-1}))

狀態轉移方程:
用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] 位。
在這裏插入圖片描述
具體步驟:

  1. 尋找前綴後綴最長公共元素長度,如果模式串爲“abab”,那麼它的各個子串的前綴後綴的公共元素的最大長度如下:
    在這裏插入圖片描述
  2. 求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層投下玻璃珠,最壞的嘗試次數是:max(n,F(Nn)+1);max(n,F(N-n)+1);
那麼所有層投下的最壞嘗試次數的最小值即爲問題的解:
F(N)=min(max(1,1+F(N1)),max(2,1+F(N2)),...,max(N1,1+F(1))).F(1)=1F(N)=min(max(1,1+F(N-1)),max(2, 1+F(N-2)),...,max(N-1,1+F(1))).其中F(1)=1

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];
 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章