本文內容整理於胡凡老師的《算法筆記》
文章目錄
- 1 配置VS環境
- 2 C++基本語法易忘點整理
- 3 C++標準模板庫STL
- 3.1 vector向量:“邊長數組”
- 3.2 set集合:元素自動去重並按升序排列(頭文件#include)
- 3.3 string字符串
- 3.4 queue隊列:實現先進先出的容器
- 3.5 priority_queue優先隊列:隊首元素一定是當前隊列中優先級最大的
- 3.6 stack棧:實現後進先出的容器
- 3.7 algorithm中常用的函數:max,min,abs,reverse,fll,sort,lower_bound
- 3.8 map映射:類似於字典,將任何基本類型映射到任何基本類型(包括STL容器)(1) 需要引入頭文件#include
- 4 數據結構:樹
- 4.1 鏈表:數據域與指針域
- 4.2 二叉樹:根結點,左子樹,右子樹
- 4.3 二叉樹的遍歷(遞歸程序)
- 4.4 二叉樹的遍歷(非遞歸程序)
- 4.5 先序和中序重建二叉樹
- 4.6 其他:靜態二叉樹,判斷完全二叉樹,左右結點對換
- 4.7 二叉查找樹BST
- 4.8 平衡二叉樹AVL
- 4.9 並查集:維護集合的數據結構:合併、查找、集合
- 4.10 大頂堆
- 4.11 堆排序(遞增)
- 4.12 哈夫曼樹
- 5 數據結構:圖
- 5.1 鄰接矩陣G[][]
- 5.2 鄰接表
- 5.3 圖的遍歷:深度優先搜索DFS
- 5.4 圖的遍歷:廣度優先遍歷BFS
- 5.5 單源最短路徑:Dijkstra算法
- 5.6 單源最短路徑:Floyd算法
- 5.7 最小生成樹
- 5.8 最小生成樹:prim算法
- 5.9 最小生成樹:kruskal算法
- 5.10 拓撲排序
- 5.11 關鍵路徑
- 6 字符串匹配:KMP算法
- 7 排序算法
1 配置VS環境
1.1 高版本默認不能使用scanf函數
配置:項目->屬性->C/C+±>預處理器->預處理器定義->輸入:_CRT_SECURE_NO_WARNINGS;
1.2 輸入齊全的頭文件
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<stack>
#include<fstream>
#include<sstream>
using namespace std;
2 C++基本語法易忘點整理
- scanf和printf 比 cin和cout快的多;不要同時在一個程序中使用cout和printf
- %d:int型;
%lld:long long型
%f:(printf)float和double型;(scanf)float
%lf:(scanf)double型
%c:char型;能夠識別空格和換行並將其輸入
%s:字符串型;通過空格或換行來識別一個字符串的結束 - 整型:
(1)int型:32位,絕對值在10的9次方範圍以內的整數
(2)long long型:64位,數值範圍超過10的10次方 - 不要使用float,碰到浮點數的數據都應該用double來存儲
- 小寫字母比大寫字母的ASCII碼值大32
- printf("%c", 7); ASCII碼爲7是控制響鈴功能的控制字符
- 無窮大常用設置:
const int INF = (1<<30) - 1;
const int INF = 0x3fffffff;
const int inf = 1000000000;(8個0) - &:取地址運算符
- scanf中輸入char數組,不需要加&,因爲數組名稱本身代表數組第一個元素的地址
- 輸入格式樣式:"%d,%d,%d"可接受 a,b,c 這樣的輸入
"%d:%d:%d"可接受 a🅱️c這樣的輸入
"%d%d%d"可接受用空格隔開的輸入 - scanf的%c格式是可以讀入空格和換行符。
- 輸出格式:
%.mf:保留m位小數輸出;
%md:右對齊輸出,不足m位則高位補空格,否則不變輸出
%0md:不同%md是,高位補0 - getchar()輸入單個字符;putchar()輸出單個字符;
gets_s用來輸入一行字符串,識別換行符作爲輸入結束
puts用來輸出一行字符串,並緊跟一個換行 - math函數
fabs(double x):double型變量取絕對值
floor(double x)和ceil(double x):向下取整和向上取整
log(double x):以自然對數爲底的對數
round(double x):四捨五入 - 如果數組大小較大(大概10的6次方級別),則需要將其定義在主函數外面,否則會使程序異常退出,函數外面申請的全局變量來自靜態存儲區
- memset:對所有數組賦值相同的0或-1(添加頭文件cstring)
- 字符數組注意事項
(1)結束符\0的ASCII碼爲0,即空字符NULL,佔用一個字符位,因此開字符數組的時候
千萬記得字符數組的長度一定要比實際存儲字符串的長度至少多1;
(2)使用getchar輸入字符串時,記得在每個字符串後面加上“\0” - ASCII碼:
09:4857;
AZ:6590;
az:97122; - string.h頭文件函數
(1)strlen():可以得到字符串長度
(2)strcmp():按字典順序比較兩個字符串
(3)strcpy(字符數組1, 字符數組2):將數組2複製給數組1
(4)strcat():連接兩個字符串
(5)sscanf(str, “%d”, &n); 將字符數組str中的內容以"%d"的格式寫到n中
(6)sprintf(str, “%d”, n); n以"%d"的格式寫到str字符數組中
舉例1:將字符數組str中的內容按"%d:%lf,%s"的格式寫到int型變量n,double型變量db,
char型數組str2中
char str[100] = “2048:3.14,hello”, str2[100];
sscanf(str, “%d:%lf,%s”, &n, &db, str2);
舉例2:將int型變量n,double型變量db,char型數組str2按照"%d:%lf,%s"的格式寫入字符數組str中
sprintf(str, “%d:%lf,%s”, n, db, str2); - 指針:
變量的地址:&a
定義指針變量並初始化:int *pa = &a;
取值:b = *pa; - 引用,實參設置爲int &x:則改變值則會影響函數外
- 多點測試
(1)while…EOF型
while(scanf("%d", &n) != EOF){…}
其中EOF爲-1,效果爲無限輸入處理
ps:whle(gets_s(str))同樣可以
(2)while…break型
while(scanf("%d%d", &a, &b), a||b){} 直到輸入的a和b同時爲0,才結束while
(3)while(T-)型
3 C++標準模板庫STL
3.1 vector向量:“邊長數組”
(1) vector定義
vector<typename> name;
//例如
vector<int> name; //int型的一維可變數組
vector<vector<int> > name; //int型的二維可變數組
vector<int> vi[100]; //int型的二維數組,其中一維是固定爲100大
(2) vector元素訪問:
a、 通過下標訪問(類似數組):vi[index],下標從0到vi.size()-1
b、 通過迭代器訪問:
迭代器(類似指針)定義:
vector<typename>::iterator it = vi.begin(); //begin()爲vi的首元素地址
訪問元素: *(it + i) //等同於vi[i]; ==
注:it支持自增自減,循環條件只能用it != vi.end()== //end()爲尾元素的下一個地址。
(3) Vector常用函數:(vector向量vi)
- vi.push_back(x):在vector後面添加一個元素x
- vi.pop_back():刪除vector的尾元素
- vi.size():獲得元素個數
- vi.clear():清空所有元素
- insert(it, x):向指定位置的迭代器it處插入一個元素x
- erase(it):刪除迭代器爲it處的元素;
- erase(first, last):刪除[first, last)的所有元素(不包含last位置元素)
3.2 set集合:元素自動去重並按升序排列(頭文件#include)
(1) set定義:
set<typename> name;
(2) set元素訪問: 只能通過迭代器訪問:
set<typename>::iterator it = vi.begin();
注:除了vector和string外,STL容器都不支持*(it + i)的訪問
(3) set函數:(set集合st)
- st.insert(x):將x插入集合中
- st.find(value):返回set中對應值爲value的迭代器
- st.erase(it):it爲所需要刪除元素的迭代器
st.erase(value):value爲所需要刪除元素的值
st.erase(first, last):刪除迭代器範圍爲[first, last)的元素 - st.size():得到元素個數
- clear():清空所有元素
3.3 string字符串
(1) string定義
string str = “abcd”;
(2) string元素訪問:
- 下標訪問,鍵入和打印string字符串,只能用cin和cout,或者使用str.c_str()轉換爲字符數組,才能用printf
- 通過迭代器訪問:
string::iterator it = str.begin()
注:string和vector可以直接對迭代器進行加減某個數字,其他不行
(3) String函數
- +=:可以直接將兩個string拼接起來
- 可以直接用符號比較,按照字典序
- str.length()或str.size()
- str.insert(pos, str2):在pos號位置插入字符串str2
str.insert(it, it2, it3):表示串[it2, it3)插在it的位置上 - str,erase(it):it爲需要刪除元素的迭代器
str.erase(first, last):刪除元素的迭代器區間
str.erase(pos, length):pos爲開始刪除的起始位置,length爲刪除的字符個數 - str.clear():清空字符串
- str.substr(pos, len):返回從位置pos開始,長度爲len的子串
- str.find(str2):當str2是str的子串時,返回其在str中第一次出現的位置,如果str2不是子串,則返回string::npos(值爲-1或4294967295)
- str.find(str2, pos):從str的pos號位開始匹配str2
- str.replace(pos, len, str2):把str從pos號位開始,長度爲len的子串替換成str2
- str.replace(it1, it2, str2):把str的迭代器[it1, it2)範圍的子串替換爲str2
3.4 queue隊列:實現先進先出的容器
(1)queue定義:
queue<typename> name;
(2)queue元素訪問
q.front()訪問隊首元素,q.back()訪問隊尾元素
(3)queue函數
- q.push(x):將x入隊
- q.front(), q.back():分別獲得隊首和隊尾元素
- q.pop():令隊首元素出隊
- q.empty():檢查queue是否爲空,返回true則爲空
- q.size():返回隊列內元素個數
3.5 priority_queue優先隊列:隊首元素一定是當前隊列中優先級最大的
(1)priority_queue的定義:
priority_queue<typename> name;
(2)priority_queue元素訪問:
只能用q.top()來訪問隊首元素
(3)priority_queue函數:
q.push(); q.top(); q.pop(); q.empty(); q.size();
(4)priority_queue元素優先級設定:
- 基本數據類型優先級設置:
定義:priority_queue<int, vector, less > name;
其中less 表示數字越大的優先級越大
換成greater表示數字越大的優先級越小 - 結構體的優先級設置:
例如:結構體fruit
方法一:重載小於號<: 使得隊列按照從小到大排序,與sort中的規則相反 (使用引用效率更高),記得寫在fruit結構體內
friend bool operator < (const fruit &f1, const fruit &f2)
{
return f1.price > f2.price;
}
則定義:priority_queue q;
方法二:寫在結構體外
struct cmp{
bool operator () (fruit f1, fruit f2){
return f1.price > f2.price;
}
}
則定義:priority_queue<fruit, vector, cmp> q;
3.6 stack棧:實現後進先出的容器
(1)定義:
stack<typename> name;
(2)stack元素訪問:
只能通過st.top()來訪問棧頂元素
(3)stack函數:
st.push(x); st.top(); st.pop(); st.empty(); st.size();
注:stack中沒有清空元素的操作,則可以:
while(!st.empty()){
st.pop();
3.7 algorithm中常用的函數:max,min,abs,reverse,fll,sort,lower_bound
- max(x, y); min(x, y); abs(x, y); swap(x, y);
- everse(it, it2):將數組指針在[it, it2)之間的元素或容器的迭代器內的元素反轉
- fill(it, it2, value):將數組指針或容器迭代器在[it, it2)內的元素全部賦值爲value;
- sort(首元素地址, 尾元素地址的下一個地址, 比較函數cmp(選填));sort默認遞增排序
A. 基本數據類型數組的排序
定義從大到小排序
bool cmp(int a, int b){
return a > b;
}
注:>表示從大到小,<表示從小到大
B. 結構體數組的排序:舉例結構體node
bool cmp(node a, node b){
return a.x > b.x;
}
C. STL容器:vector,string,deque
sort(it, it2, cmp)
- lower_bound(first, last, val):用來尋找在數組或容器[first, last)範圍內第一個值大於等於val的元素的位置,返回的是指針或迭代器
- upper_bound(first, last, val):與上面不同的是,返回的是第一個值大於val,注:若沒有找到元素,則返回可以插入該元素的位置的指針或迭代器。
3.8 map映射:類似於字典,將任何基本類型映射到任何基本類型(包括STL容器)(1) 需要引入頭文件#include
- mao定義:
map<typename1, typename2> mp;
其中typename1相當於key的類型,typename2相當於value的類型
- map的元素訪問:
A. 通過下標訪問:
舉例:map<char, int> mp;
mp[‘c’] = 20;
B. 通過迭代器訪問:
map<typename1, typename2>::iterator it = mp.begin();
it -> first //表示當前映射的key
it -> second //表示當前映射的value
注:map會自動將鍵按照從小到大排序
- map函數:
- mp.find(key):返回鍵爲key的映射的迭代器
- mp.erase(it):it爲需要刪除的元素的迭代器
mp.erase(key):key爲要刪除的映射的鍵
mp.erase(first, last) - mp.size(), mp.clear()
4 數據結構:樹
4.1 鏈表:數據域與指針域
- 鏈表定義
struct node{
typename data;
node* next;
}
- 使用new爲鏈表結點分配內存空間
- 基本用法:
typename* name = new typename
- 舉例:
node* p = new node;
==注:使用完後記得釋放內存:delete(p);
- 基本用法:
- 創建鏈表:舉例
node* node1 = new node;
node* node2 = new node;
node1->data = 5;
node1->next = node2;
node2->data = 3;
node2->next = NULL;
- 靜態鏈表:實現原理爲hash,定義結構體數組,通過下標來定位
4.2 二叉樹:根結點,左子樹,右子樹
- 概念:
- 根結點爲第一層;
- 滿足連通:邊數等於頂點數減一的結構一定是一棵樹
- 深度:從根結點(深度爲1)自頂向下
- 高度:從最底層葉子結點(高度爲1)自底向上
- 二叉樹存儲結構:
struct node{
typename data;
node* lchild, *rchild;
}
- 新建結點
node* newNode(int v)
{
node* Node = new node;
Node->data = v;
Node->lchild = Node->rchild = NULL;
return Node;
}
- 二叉樹結點的查找和修改
void search(node* root, int x, int newData)
{
if(root == NULL)
return;
if(root->data == x)
{
root->data = newData;
return;
}
else
{
search(root->lchild, x, newData);
search(root->rchild, x, newData);
}
}
注意:如果函數中需要新建結點,則需要將函數參數中的node* root
改爲node* &root
;如果只是修改當前已有結點的內容,則不需要加引用。
- 完全二叉樹的存儲結構
- 按照從上到下,從左到右進行編號(與層序遍歷的序列相同)
- 存儲的數組下標必須從1開始,下標1存放根結點
- 結點編號爲x,則左孩子結點爲2x,右孩子結點爲2x+1
4.3 二叉樹的遍歷(遞歸程序)
- 先序遍歷:根結點->左孩子->右孩子
void preOrder(node* root)
{
if(root == NULL)
return;
printf("%d ", root->data);
preOrder(root->lchild);
preOrder(root->rchild);
}
- 中序遍歷:左孩子->根結點->右孩子
void inOrder(node* root)
{
if(root == NULL)
reutrn;
inOrder(root->lchild);
printf("%d ", root->data);
inOrder(root->rchild);
}
- 後序遍歷:左孩子->右孩子->根結點
void postOrder(node* root)
{
if(root == NULL)
reutrn;
postOrder(root->lchild);
postOrder(root->rchild);
printf("%d ", root->data);
}
- 層序遍歷
思路:將根節點存入隊列中;取出隊列的首結點,訪問它;如果有左孩子,則將其入隊;如果有右孩子,則將其入隊;返回到第二步繼續
void layerOrder(node* root)
{
queue<node*> q;
node* p = root;
q.push(p);
while(!q.empty())
{
p = q.front();
q.pop();
if(p->lchild != NULL) q.push(p->lchild);
if(p->rchild != NULL) q.push(p->rchild);
}
}
4.4 二叉樹的遍歷(非遞歸程序)
- 先序遍歷
void preOrder(node* root)
{
node* p = root;
stack<Node*> st;
while(p || !st.empty())
{
while(p)
{
printf("%d ", p->data);
st.push(p);
p = p->lchild;
}
if(!st.empty())
{
p = st.top();
st.pop();
p = p->rchild;
}
}
}
- 中序遍歷
void inOrder(node* root)
{
node* p = root;
stack<node*> st;
while(p || !st.empty())
{
while(p)
{
st.push(p);
p = p->lchild;
}
if(!st.empty())
{
p = st.top();
st.pop();
printf("%d ", p->data);
p = p->rchild;
}
}
}
- 後序遍歷
思路:先一直遍歷到最左邊的結點,依次入棧;有一個標記指針,記錄最近訪問過的元素,只有當前結點滿足條件:沒有右孩子結點或者右孩子結點被訪問過時才能夠輸出(被訪問),否則,若訪問了其左孩子結點,則轉向訪問右子樹。
void postOrder(node* root)
{
node* p = root;
node* flag = NULL;
stack<node*> st;
while(p || !st.empty())
{
while(p)
{
st.push(p);
p = p->lchild;
flag = p;
}
if(!st.empty())
{
p = st.top();
if(p->rchild == NULL && p->rchild == flag)
{
printf("%d ", p->data);
st.pop();
flag = p;
p = NULL;
}
else
{
p = p->rchild;
flag = p;
}
}
}
}
4.5 先序和中序重建二叉樹
注:中序序列可以與先序序列、後序序列、層序序列中的任意一個來構建二叉樹,缺少中序序列就無法重建二叉樹
思路:假設先序序列[preL, preR]
,其中當前子樹的根結點爲preL
,其中序序列區間爲[inL, inR]
,遍歷中序序列找到點:ink == preL
,此時對於先序序列來說,其左子樹區間爲[preL +1, preL + ink - inL]
,右子樹區間爲[preL + ink – inL + 1, preR]
,對於中序序列來說,其左子樹區間爲[inL, ink-1]
,其右子樹區間爲[ink+1, inR]
,利用遞歸的思路來解決,遞歸邊界爲preL > preR
,遞歸式爲:
root->data = preSet[preL];
root->lchild = create(preL+1, preL + ink – inL, inL, ink - 1);
root->rchild = create(preL + ink – inL +1, preR, ink + 1, inR);
程序:
node* create(int preL, int preR, int inL, int inR)
{
//preSet[]爲先序序列,inSet[]爲中序序列
if (preL > preR)
return NULL;
node* root = new node;
root->data = preSet[preL];
int i;
for (i = inL; i <= inR; i++)
{
if (root->data == inSet[i])
break;
}
int numleft = i - inL;
root->lchild = create(preL + 1, preL + numleft, inL, i - 1);
root->rchild = create(preL + numleft + 1, preR, i + 1, inR);
return root;
}
另:給定中序序列和後序序列重建二叉樹
node* create_1(int postL, int postR, int inL, int inR)
{
//postSet[]爲後序序列
if (postL > postR)
return NULL;
node* root = new node;
root->data = postSet[postR];
int k;
for ( k = inL; k <= inR; k++)
{
if (root->data == inSet[k])
break;
}
int numleft = k - inL;
root->lchild = create_1(postL, postL + numleft - 1, inL, k - 1);
root->rchild = create_1(postL + numleft, postR - 1, k + 1, inR);
return root;
}
4.6 其他:靜態二叉樹,判斷完全二叉樹,左右結點對換
- 靜態二叉樹:利用結構數組的下標來替代指針的作用,例如堆,數組下標從1開始編號,2結點下標爲其左孩子,2結點下標+1爲其右孩子
- 判斷二叉樹是否爲完全二叉樹:利用層序遍歷的算法,區別是無論其結點是否爲NULL,都入隊,當出隊遇到NULL時,則判斷隊中是否還有非空指針,如有,則表示不是完全二叉樹,反之則是。
- 將二叉樹的左右結點對換:則利用後序遍歷的思路
4.7 二叉查找樹BST
- 定義:左子樹上的所有結點的數據域都小於或等於根結點的數據域,右子樹上的所有結點的數據域均大於根結點的數據域。(左子樹<根結點<右子樹)
- 新建結點:
node* newNode(int newData)
{
node* Node = new node;
Node->data = newData;
Node->lchild = Node->rchild = NULL;
return Node;
}
- 插入新結點==(記得使用引用符號&)==
void insert(node* &root, int newData)
{
if (root == NULL)
{
root = newNode(newData);
return;
}
if (newData == root->data) return;
else if (newData > root->data)
insert(root->rchild, newData);
else
insert(root->lchild, newData);
}
- 查找操作
void search(node* root, int x)
{
if (root == NULL)
{
printf("failed.\n");
return;
}
if (root->data == x)
printf("result: %d", root->data);
else if (root->data < x)
search(root->rchild, x);
else
search(root->lchild, x);
}
- 找到最小結點
node* findMin(node* root)
{
while (root->lchild != NULL)
{
root = root->lchild;
}
return root;
}
- 找到最大結點
node* findMax(node* root)
{
while (root->rchild != NULL)
{
root = root->rchild;
}
return root;
}
- 刪除結點:先找到被刪除結點的前驅結點(該結點的左子樹中的最右結點)或者後驅結點(該結點的右子樹中的最左結點),並用結點來代替被被刪除結點的位置,然後同樣的去刪除前驅結點或後驅結點,直到刪除到葉結點,則直接刪除。利用遞歸函數,傳入當前結點參數要用引用&。
void deleteNode(node*& root, int x)
{
if (root == NULL) return;
if (root->data == x)
{
if (root->rchild == NULL && root->lchild == NULL)
root = NULL;
else if (root->lchild != NULL)//有左孩子
{
node* pre = findMax(root->lchild);//前驅
root->data = pre->data;
deleteNode(root->lchild, pre->data);
}
else
{
node* next = findMin(root->rchild);//後驅
root->data = next->data;
deleteNode(root->rchild, next->data);
}
}
else if (root->data > x)
deleteNode(root->lchild, x);
else
deleteNode(root->rchild, x);
}
- 性質:其中序序列是有序的
4.8 平衡二叉樹AVL
- 定義:仍然是二叉查找樹,其左子樹與右子樹的高度之差的絕對值不能超過1(平衡因子)
struct node
{
int v, height;//v爲結點權值,height爲當前子樹的高度
node* lchild, * rchild;
};
- 新建結點
node* newNode(int v)
{
node* Node = new node;
Node->v = v;
Node->lchild = Node->rchild = NULL;
Node->height = 1;//結點高度初始爲1
}
- 獲得當前結點的高度
int getHeight(node* root)
{
if (root == NULL) return 0;
return root->height;
}
- 計算平衡因子
int getBalanceFactor(node* root)
{
return getHeight(root->lchild) - getHeight(root->rchild);
}
- 更新高度
void updateHeight(node* root)
{
root->height = max(getHeight(root->lchild),
getHeight(root->rchild)) + 1;
}
- 查找操作:與二叉查找樹相同
- 左旋(RR樹型時使用)都要記得更新高度root和temp的高度
void L(node*& root)
{
node* temp = root->rchild;
root->rchild = temp->lchild;
temp->lchild = root;
updateHeight(root);
updateHeight(temp);
root = temp;
}
- 右旋(適用LL樹型)
void R(node*& root)
{
node* temp = root->lchild;
root->lchild = temp->rchild;
temp->rchild = root;
updateHeight(root);
updateHeight(temp);
root = temp;
}
注:主要把最靠近插入結點的失衡結點調整到正常,路徑上的所有結點就都會平衡
- 左旋右旋(適用於LR樹型)
- 左旋右旋(適用RL樹型)
- AVL插入情況彙總
樹型 | 判定條件(BF爲平衡因子) | 調整方法 |
---|---|---|
LL | BF(root)=2, BF(root->lchild)=1 | 對root進行右旋 |
LR | BF(root)=2, BF(root->lchild)=-1 | 先對root->lchild進行左旋,再對root進行右旋 |
RR | BF(root)=-2, BF(root->rchild)=-1 | 對root進行左旋 |
RL | BF(root)=-2, BF(root->rchild)=1 | 先對root->rchild進行右旋,再對root進行左旋 |
- 考慮平衡因子的插入結點操作
void insert(node* root, int v)
{
if (root == NULL)
{
root = newNode(v);
return;
}
if (v < root->v)
{
insert(root->lchild, v);
updateHeight(root);
if (getBalanceFactor(root) == 2)
{
if (getBalanceFactor(root->lchild) == 1)
{
R(root);
}
else if (getBalanceFactor(root->lchild) == -1)
{
L(root->lchild);
R(root);
}
}
}
else
{
insert(root->rchild, v);
updateHeight(root);
if (getBalanceFactor(root) == -2)
{
if (getBalanceFactor(root->rchild) == -1)
L(root);
else if (getBalanceFactor(root->rchild) == 1)
{
R(root->rchild);
L(root);
}
}
}
}
4.9 並查集:維護集合的數據結構:合併、查找、集合
- 定義:
int father[N];
其中father[N]
表示元素i的父親結點,而father[i] = i
表示爲根結點
例如:
father[1] = 1 //父親結點是自己本身,則爲根結點
father[2] = 1 //2的父親結點是1
- 並查集的初始化
void initialize(int* father, int N)
{
for (int i = 1; i <= N; i++)
{
father[i] = i;
}
}
- 並查集的查找:尋找其根結點
int findFather(int father[], int x)
{
while (x != father[x])
{
x = father[x];
}
return x;
}
- 集合的合併:將兩個不用根結點的結點合併,即合併其根結點
void Union(int *father, int f1, int f2)
{
int f1f = findFather(father, f1);
int f2f = findFather(father, f2);
if (f1f != f2f) father[f1f] = f2f;
}
- 並查集產生的每一個集合都是一棵樹,不會產生環
- 路徑壓縮:把當前查詢結點路徑上的所有結點的父親都指向根結點
4.10 大頂堆
大頂堆:每個結點的值都不小於其左右孩子結點的值,用於實現優先隊列
- 當前結點向下調整
void downAdjust(int low, int high)
{
int i = low, j = i * 2;//i爲要調整結點,j爲i的左孩子結點
while (j <= high)
{
//左右孩子中最大的
if (j + 1 <= high && heap[j + 1] > heap[j])
{
j = j + 1;
}
//交換位置
if (heap[i] < heap[j])
{
swap(heap[i], heap[j]);
i = j;
j = 2 * i;
}
else
break;
}
}
- 建堆:從第一個非葉子結點向上調整
void createHeap()
{
for (int i = n/2; i >=1; i--)
{
downAdjust(i, n);
}
}
- 刪除堆頂元素:只需要將最後一個元素覆蓋堆頂元素,然後對其向下調整
void deleteTop()
{
heap[1] = heap[n--];
downAdjust(1, n);
}
- 添加一個元素:將新元素放在數組的最後,然後對其向上調整
- 向上調整程序
void upAdjust(int low, int high)
{
int i = high, j = i / 2;//i爲調整結點,j爲其父結點
while (j >= low)
{
if (heap[i] > heap[j])
{
swap(heap[i], heap[j]);
i = j;
j = i / 2;
}
else
break;
}
}
+ 添加元素程序
void insert(int x)
{
heap[++n] = x;
upAdjust(1, n);
}
4.11 堆排序(遞增)
思路:每次將最後一個結點與堆頂結點交換,交換完後用層序遍歷輸出,得到遞增序列
void heapSort()
{
for (int i = n; i >1; i--)
{
swap(heap[i], heap[1]);
downAdjust(1, i - 1);
}
}
4.12 哈夫曼樹
- 概念
- 帶權路徑長度:葉子結點的權值乘以其路徑長度的結果
- 樹的帶權路徑長度WPL:所有葉子結點的帶權路徑長度之和
- 哈夫曼樹:帶權路徑長度最小的樹,也稱最優二叉樹
- 哈夫曼樹不一定是唯一的,但是最小帶權路徑長度是唯一的
- 構建一個哈夫曼樹思路
合併其中權值最小的兩個結點,生成其父結點,權值爲這兩個結點之和,把父結點放回去,再重複抽兩個最小結點,直到最後一個結點。 - 計算最小帶權路徑長度WPL
思路:每次都將選擇出的兩個結點權值一直累加並將總值入隊,直到優先隊列中的最後一個結點的權值不加。
int main()
{
int n, temp;
int x, y, wpl = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%d", &temp);
q.push(temp);
}
while (q.size() > 1)
{
x = q.top();
q.pop();
y = q.top();
q.pop();
q.push(x + y);
wpl += (x + y);
}
printf("wpl: %d", wpl);
}
5 數據結構:圖
5.1 鄰接矩陣G[][]
G[i][j] = 1;
表示頂點i和j之間有邊(值可爲權重);只適用於頂點數目不大的題目
5.2 鄰接表
對於一個圖來說,每個頂點都可能有若干條出邊,如果把同一個頂點的出邊放在一個列表中,那麼N個頂點就有N個列表,這個N個列表被稱爲圖G的鄰接表
- 如果鄰接表只存放每條邊的終點編號,鄰接表可定義爲
vector<int> Adj[N];
添加一條從1號頂點到3號頂點的有向邊:Adj[1].push_back(3);
- 如果需要同時存放終點編號和邊權,則通過結構體定義鄰接表:
struct Node{
int v; //邊的終點編號
int w;//邊權
Node(int _v, int w_) : v(_v), w(_w) {} //構造函數
}
vector<Node> Adj[N];
添加一條從1號頂點到3號頂點權值爲2的有向邊Adj[1].push_back(Node(3,2));
5.3 圖的遍歷:深度優先搜索DFS
- 概念
- 思想:沿着一條路徑直到無法繼續前進,才退回到路徑上離當前結點最近的還存在未訪問分支頂點的岔路口,並前進訪問那些未訪問分支頂點,直到遍歷整個圖
- 連通分量:在無向圖中,任意兩個頂點可以互相到達,則圖G爲連通圖,其中最大連通子圖爲連通分量。
- 強連通分量:在有向圖中,任意兩個頂點可以互相到達,則圖G爲強連通圖,其中極大強連通子圖爲強連通分量。
- DFS僞代碼
//訪問頂點u
DFS(u){
vis[u] = true; //設置結點u已被訪問
for( 從u出發能到達的所有結點v)
if( vis[v] == false)
DFS(v);
}
//遍歷圖G
DFSTrave(G){
for( 圖G的所有結點u)
DFS(u);
}
5.4 圖的遍歷:廣度優先遍歷BFS
- 概念
+思想:建立一個隊列,並把初始頂點加入到隊列中,此後每次都取出隊列的首頂點進行訪問,並把該頂點可以到達的未曾加入隊列的頂點全部加入到隊列中,直到隊列爲空。 - BFS僞代碼
BFS(u){
queue q;//定義隊列q
將u入隊
inq[u] = true;//設置u已被加入過隊列
while(q非空){
取出q的隊首元素u進行訪問
for( 從u出發到達的所有結點v){
if( inq[v] == false){
將v入隊
inq[v] = true;
}
}
}
}
BFSTravel(G){
for(G的所有結點)
if(inq[u] == false)
BFS(u);
}
5.5 單源最短路徑:Dijkstra算法
- 概念:解決“單源最短路”問題:給定圖G和起點s,通過算法得到S到達其他每個頂點的最短距離。並且要求邊的權值爲非負數。
- Dijkstra 算法的策略
設置集合S存放已被訪問的頂點(即已訪問的頂點),然後執行n次下面的兩個步驟(n爲頂點個數)- 每次從集合V-S(即未被訪問的頂點)中選擇與起點s的最短距離最小的一個頂點(記爲u),訪問並加入集合S(即已被訪問)
- 之後,令頂點u爲中介點,優化起點s與所有從u能到達的頂點v之間的最短路徑
- Dijkstra 算法的具體實現:
- 集合S用一個bool型數組vis[]實現
- int型的d[]表示起點到達頂點v的距離,初始時除了給起點s的d[s]賦值爲0,其他頂點可以賦值一個很大的數(1000000000,10的9次方)來表示inf
- int型數組pre[]記錄當前結點的前驅結點
- Dijkstra 算法的僞代碼
Dijstra(G, d[], s){
初始化;
for(循環n次){
u=使d[u]最小的還未被訪問的頂點的標號;
記u已被訪問;
for(u出發能夠達到的所有頂點v)
if(v未被訪問過 && 以u爲中介點使s到達v的最短距離d[v]更優){
優化d[v];
記錄前驅結點pre[];
}
}
}
- 鄰居矩陣版代碼
void Dijkstra(int s)//s爲起點
{
//初始化
fill(d, d + maxn, inf);
d[s] = 0;
for (int i = 0; i < maxn; i++)
{
int u = -1, min = inf;//min = d[u]
for (int j = 0; j < maxn; j++)
{
if (vis[j] == false && min > d[i])
{
min = d[i];
u = j;
}
}
//找不到小於INF的d[u],表示剩下的頂點和起點s不連通
if (u == -1) return;
//以下部分區別於鄰接矩陣和鄰接表
for (int v = 0; v < maxn; v++)
{
if (vis[v] == false && G[u][v] != inf && d[u] + G[u][v] < d[v])
{
d[v] = d[u] + G[u][v];
pre[v] = u;
}
}
}
}
- 鄰接表版代碼 修改以上註釋說明的部分
for (int j = 0; j < Adj[u].size(); j++)
{
int v = Adj[u][j].v;
if (vis[v] == false && d[u] + Adj[u][j].dis < d[v])
{
d[v] = d[u] + Adj[u][j].dis;
pre[v] = u;
}
}
- 輸出最短路徑
void DFS(int s, int v)//s爲源點,v爲當前訪問的結點
{
if (v == s) {
printf("%d\n", v);
return;
}
DFS(s, prev[v]);
printf("%d\n", v);
}
5.6 單源最短路徑:Floyd算法
- 定義:用來解決全源最短路問題,即給定圖G,求任意兩點u,v之間的最短路徑長度,時間複雜度n的三次方,圖適合使用鄰接矩陣。
- Floyd算法的思路:
如果存在頂點k,使得以k作爲中介點時頂點i和頂點j的當前最短距離縮短,則使用頂點k作爲頂點i和頂點j的中介點。 - Floyd算法流程
枚舉頂點k
以頂點k作爲中介點,枚舉所有頂點對i和j
如果dis[j][k] + dis[k][i] < dis[i][j]成立
賦值dis[i][j] = dis[j][k] + dis[k][i];
- Floyd算法實現
void Floyd()
{
for (int k = 0; k < n; k++)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (dis[i][k] != inf && dis[k][j] != inf &&
dis[i][k] + dis[k][j] < dis[i][j])
{
dis[i][j] = dis[i][k] + dis[k][j];
}
}
}
}
}
5.7 最小生成樹
- 概念:是一個給定的無向圖中求一棵樹T,使得這棵樹擁有圖G中的所有頂點,且所有邊都是來自圖G的邊,並且滿足整棵樹的邊權之和最小。
- 性質:
- 最小生成樹是樹,因此其邊數等於頂點數減一,且樹內一定不會有環
- 對給定的圖,其最小生成樹不唯一,但其邊權之和一定是唯一的
- 由於最小生成樹是在無向圖上生成的,因此根結點可以是這棵樹上的任意一個結點,按照實際題目要求來
- 求解最小生成樹有兩種算法:prim算法,kruskal算法
5.8 最小生成樹:prim算法
- prim算法
- 使用vis[]表示頂點是否已被訪問
- int型數組d[]來存放結點v與集合S的最短距離,初始時除了起點S的d[s]賦值爲0,其他都爲inf
- ==prim算法與Dijkstra算法使用的思想幾乎完全相同,只是數組d[]的含義上有所區別。==其中Dijkstra算法的數組d[]含義爲起點s到達頂點v的最短距離,而prim算法的數組d[]含義爲頂點v與集合S的最短距離
- prim算法僞代碼
prim(G, d[]){
初始化;
for(循環n次){
u=使d[u]最小的還未被訪問的頂點的標號;
記u已被訪問;
for(從u出發能到達的所有頂點v)
if(v未被訪問 && 以u爲中介點使得v和集合S的最短距離d[v]更優)
將G[u][v]賦值給v與集合S的最短距離d[v];
}
}
- 鄰接矩陣版代碼(參考Dijkstra算法,只是d[]的含義不同)
const int maxn = 1000;
const int inf = 1000000000;
bool vis[maxn] = { false };
int d[maxn];
int G[maxn][maxn];
int n;
int prim()
{
int ans = 0;
fill(d, d + maxn, inf);
d[0] = 0;
for (int i = 0; i < n; i++)
{
int u = -1, MIN = inf;
for (int j = 0; j < n; j++)
{
if (vis[j] == false && d[j] < MIN)
{
u = j;
MIN = d[j];
}
}
//若沒有找到任何一個不爲inf的d[u],則剩下的頂點與集合S不連通
if (u == -1) return -1;
vis[u] = true;
ans += d[u];//將與集合s距離最小的邊加入最小生成樹
//以下區別鄰接矩陣與鄰接表
for (int v = 0; v < n; v++)
{
if (vis[v] == false && G[u][v] != inf && G[u][v] < d[v])
{
d[v] = G[u][v];
}
}
}
return ans;
}
- 鄰接表版本代碼修改以上註釋說明的地方
for (int j = 0; j < Adj[u].size(); j++)
{
int v = Adj[u][j].v;
if (vis[v] == false && Adj[u][j].dis < d[v])
{
d[v] = Adj[u][j].dis;
}
}
5.9 最小生成樹:kruskal算法
- kruskal算法思路
- 對所有邊按照邊權從小到大進行排序
- 按邊權從小到大測試所有的邊,如果當前測試邊所連接的兩個頂點不在同一個連通塊,則把這條測試邊加入當前最小生成樹,否則,將邊捨棄
- 執行步驟二,直到最小生成樹中的邊數等於總頂點數減一或是測試完所有的邊時結束,而當結束時如果最小生成樹的邊數小於總頂點數減一,則說明該圖不連通
- kruskal算法定義的結構:
需要定義一個結構體來定義邊(包括邊的端點和邊權)
struct edge
{
int u, v;//表示邊的兩個頂點
int cost;//邊權
} E[MAXE];//最多有MAXE條邊
還需要對邊進行排序,因此頂一個sort的比較函數
bool cmp(edge a, edge b)
{
return a.cost < b.cost;
}
判斷兩個端點是否是同一個連通塊,則用到集合,通過並查集來實現:int father[N];
- kruskal算法的僞代碼
int Kruskal(){
令最小生成樹之和爲ans、最小生成樹當前邊數爲Num_Edge;
將所有邊按邊權從小到大排序;
for(從小到大枚舉所有邊){
if(當前測試邊的兩個端點在不同的連通塊時){
將該測試邊加入到最小生成樹中;
ans += 測試邊的邊權;
最小生成樹的當前邊數Num_Edge +1;
當邊數Num_Edge等於頂點數減一時結束循環;
}
}
return ans;
}
- 實現代碼
int father[N];
int findFather(int x)
{
...
}
int kruskal(int n, int m)//n爲頂點數,m爲邊數
{
int ans = 0, Num_Edge = 0;
for (int i = 0; i < n; i++)
{
father[i] = i;
}
sort(E, E + m, cmp);
for (int i = 0; i < m; i++)
{
int faU = findFather(E[i].u);
int faV = findFather(E[i].v);
if (faU != faV)
{
father[faU] = faV;
ans += E[i].cost;
Num_Edge++;
if (Num_Edge == n - 1) break;
}
}
return ans;
}
5.10 拓撲排序
- 概念:拓撲排序是將有向無環圖G的所有頂點排成一個線性序列,使得對圖G中的任意兩個端點u, v,如果存在邊u->v,那麼在序列中u一定在v前面。
- 應用:判斷一個給定的圖是否是有向無環圖
- 思路
- 定義一個隊列Q,並把所有入度爲0的結點加入隊列
- 取隊首結點,輸出,並刪去所有從它出發的邊,並令這些邊到達的頂點的入度減一,如果某個頂點的入度減爲0,則將其加入隊列
- 反覆進行步驟二,直到隊列爲空,如果隊列爲空時入過隊的結點數恰好爲N,說明拓撲排序成功,圖G爲有向無環圖,否則,拓撲排序失敗,圖G中有環
- 鄰接表版代碼
bool topologicalSort()
{
int num = 0;//記錄加入拓撲序列的頂點數
queue<int> q;
for (int i = 0; i < n; i++)
{
if (inDegree[i] == 0)
q.push(i);
}
while (!q.empty())
{
int u = q.front();
q.pop();
for (int i = 0; i < G[u].size(); i++)
{
int v = G[u][i];
inDegree[v]--;
if (inDegree[v] == 0)
q.push(v);
}
G[u].clear();
num++;
}
if (num == n) return true;
else return false;
}
5.11 關鍵路徑
- 概念:
- 頂點活動網AOV:用頂點表示活動,邊集表示活動間的優先關係的有向圖(可以轉換成AOE圖)
- 邊活動圖AOE:用帶權的邊集表示活動,用頂點表示事件的有向圖,其中邊權表示完成活動需要的時間;其表示的是一個工程的進行過程;都是有向無環圖;
- AOE網主要解決兩個問題:
- 工程起始到終點至少需要多少時間
- 哪條(些)路徑上的活動被稱爲關鍵路徑,關鍵路徑上的活動稱爲關鍵活動,其中AOE網中最長路徑被稱爲關鍵路徑。
- 思路整理
- 設置數組e和l,其中
e[r]
和l[r]
分別表示活動ar的最早開始時間和最遲開始時間(如不必須保留,可以不設置數組,最後可以直接通過公式推斷) - 求出以上兩個數組後,就可以通過判斷
e[r] == l[r]
是否成立來確定活動r是否是關鍵活動 - 設置數組ve和vl,其中
ve[i]
和vl[i]
分別表示事件i的最早發生時間和最遲發生時間 - 對於活動ar來說,只要在事件Vi最早發生時馬上開始,就可以使得活動ar的開始時間最早,因此:
e[r] = ve[i]
- 如果
l[r]
時活動的最遲發生時間,那麼l[r] + length[r]
就是事件Vj的最遲發生時間(length[r]
表示活動ar的邊權),因此:l[r] = vl[j] - length[r];
通過以上分析,可以將求解數組e和l,轉變成求解數組ve和vl;
- 設置數組e和l,其中
- 求解過程
- 有k個事件
Vi1 ~ Vik
通過相應的活動ar1 ~ ark
到達事件Vj,活動的邊權爲length[r1] ~ length[rk]
。假設已經算好了事件Vil ~ Vik
的最早發生時間ve[i1]~ve[ik]
,那麼事件Vj的最早發生時間爲max(ve[i1] + length[r1], …, ve[ik] + length[rk])
通過拓撲排序來求解
- 有k個事件
stack<int> topOrder;//用棧來記錄拓撲排序
bool topologicalSort()
{
queue<int> q;
for (int i = 0; i < n; i++)
{
if (inDegree[i] == 0)
q.push(i);
}
while (!q.empty())
{
int u = q.front();
q.pop();
topOrder.push(u); //將u加入拓撲序列
for (int i = 0; i < G[u].size(); i++)
{
int v = G[u][i].v;
inDegree[v]--;
if (inDegree[v] == 0)
q.push(v);
//用ve[u]來更新u的後繼結點v
if (ve[u] + G[u][i].w < ve[v])
{
ve[v] = ve[u] + G[u][i].w;
}
}
}
if (topOrder.size() == n) return true;
else return false;
}
從事件Vi出發通過相應的活動ar1 ~ ark
可以到達k個事件Vj1 ~ Vjk
活動邊權爲length[r1] ~ length[rk]
。假設已經算好了事件Vj1 ~ Vjk
的最遲發生時間vl[j1]~vl[jk]
,那麼事件Vi的最遲發生時間爲:min( vl[j1]-length[r1],…, vl[jk] – length[rk])
通過步驟a計算出的拓撲序列棧,來實現從後向前
//直接使用topOrder出棧即爲逆拓撲序列,求解vl數組
while (!topOrder.empty())
{
int u = topOrder.top();
topOrder.pop();
for (int i = 0; i < G[u].size(); i++)
{
int v = G[u][i].v; //u的第i個後繼結點v
if (vl[v] - G[u][i].w < vl[u])
vl[u] = vl[v] - G[u][i].w;
}
}
- 總代碼
//關鍵路徑,不是有向無環圖則返回-1,否則返回關鍵路徑長度
int CriticalPath()
{
memset(ve, 0, sizeof(ve));//將ve數組初始化爲0;
if (topologicalSort() == false)//求解ve數組
return -1;//不是有向無環圖
//不知道匯點則,取ve數組的最大值來初始化
fill(vl, vl + n, ve[n - 1]);//v1數組初始化爲匯點的ve值
//直接使用topOrder出棧即爲逆拓撲序列,求解vl數組
while (!topOrder.empty())
{
int u = topOrder.top();
topOrder.pop();
for (int i = 0; i < G[u].size(); i++)
{
int v = G[u][i].v; //u的第i個後繼結點v
if (vl[v] - G[u][i].w < vl[u])
vl[u] = vl[v] - G[u][i].w;
}
}
//遍歷鄰接表的所有邊,計算活動最早開始時間e和最遲開始時間l
for (int u = 0; u < n; u++)
{
for (int j = 0; j < G[u].size(); j++)
{
int v = G[u][j].v, w = G[u][j].w;
int e = ve[u], l = vl[v] - w;
if (e == l)
printf("%d->%d\n", u, v);//輸出關鍵活動
}
}
return ve[n - 1];//返回關鍵路徑長度
}
6 字符串匹配:KMP算法
6.1 next數組求解
next[i]
:- 表示使子串
s[0...i]
的前綴s[0...k]
等於後綴s[i-k, k]
的最大k - k爲所求最長相等前後綴中前綴的最後一位的下標,找不到相等前後綴,則令
next[i] = -1;
- 每次求出
next[i]
時,總讓j指向next[i]
;
- 表示使子串
- next數組求解過程
- 初始化next數組,令
j = next[0] = -1;
- 讓
i
在1 ~ len-1
範圍遍歷,對每個i
,執行步驟三四,以求解next[i]
- 不斷令
j = next[j]
,直到j
回退到-1
,或是s[i] == s[j + 1]
成立 - 如果
s[i] == s[j + 1]
成立, 則令next[i] = j+1;
否則令next[i] = j;
- 初始化next數組,令
- 程序:
void getNext(char s[], int len)
{
int j = -1;
next[0] = -1;
for (int i = 1; i < len; i++)
{
if (j != -1 && s[i] != s[j + 1])
{
j = next[j];
}
if (s[i] == s[j + 1])
j++;
next[i] = j;
}
}
6.2 KMP算法
next[]
數組的作用:就是當j+1
失配時,j應該回退到的位置- KMP算法思路
- 初始化
j = -1;
,表示pattern(匹配串)當前已被匹配的的最後位 - 讓
i
遍歷文本串text,對每個i
,執行步驟三四來試圖匹配text[i]
和pattern[j+1]
- 不斷令
j = next[j];
,直到j = -1
,或是text[i] == pattern[j+1]
成立 - 如果
text[i] == pattern[j+1]
成立,則令j++
,如果j
達到m-1
(m爲匹配串長度),則說明pattern是text的子串,返回true
- 初始化
- 代碼
bool KMP(char text[], char pattern[])
{
int n = strlen(text), m = strlen(pattern);
getNext(text, n);
int j = -1;
for (int i = 0; i < n; i++)
{
while (j != -1 && text[i] != pattern[j + 1])
{
j = next[j];
}
if (text[i] == pattern[j + 1])
j++;
if (j == m - 1)
return true;
}
return false;
}
7 排序算法
7.1 直接插入排序(插入排序)
- 思路:
對序列A的n個元素A[1]~A[n]
,令i從2到n枚舉,進行n-1趟操作。假設某一趟時,序列A的前i-1
個元素A[1]~A[i-1]
已經有序,而範圍A[i]~A[n]
無序,那麼該趟從範圍[1, i-1]
中尋找某個位置j
,使得A[i]
插入位置j
後,範圍[1, i]
有序。 - 代碼
void insertSort(int *A, int n)
{
//直接插入排序:從小到大
for (int i = 2; i <= n; i++)//n-1趟
{
int temp = A[i];
int j = i;
while (j > 1 && A[j - 1] > temp)
{
A[j] = A[j - 1];
j++;
}
A[j] = temp;
}
}
7.2 希爾排序(插入排序)
- 思路:
將一個數組A[]
,將其中等間隔的元素組成一組(元素下標相差gap
),並對組內元素進行插入排序,之後再將gap
減小,再組內排序,直至gap
爲1
,最後一次排序。 - 代碼
void insertSort(int A[], int gap, int i)
{
int x = A[i];
int j;
for (j = i - gap; j >= 0 && A[j] > x; j-=gap)
{
A[j + gap] = A[j];
}
A[j + gap] = x;
}
void shellSort(int A[], int n)
{
for (int gap = n/2; gap >= 1; gap/=2)
{
for (int i = gap; i < n; i++)
{
insertSort(A, gap, i);
}
}
}
7.3 冒泡排序(交換排序)
- 思路:
(效率最低)每次通過交換的方式把當前剩餘元素的最大值移動到一端,當剩餘元素減少到0時,排序結束 - 代碼
void bubbleSort(int a[], int N)
{
for (int i = 1; i < N; i++) //n-1趟
{
bool changed = false;
for (int j = 0; j < N - i; j++)
{
if (a[j] > a[j + 1])
{
swap(a[j], a[j + 1]);
changed = true;
}
}
if (changed == false)
{
break;
}
}
}
7.4 快速排序(交換排序,分治思維)
- 思路
利用分治的思維,給定一個待排序數組A[left, right]
,將哨兵x=A[left]
,設一個標記i
指向A[left]
,另一個標記j
指向A[right]
。首先將j
向前移動,找到第一個比x
小的元素,將其覆蓋A[i]
的值,然後將i
向後移動,找到第一個比x
大的元素,將其覆蓋A[j]
的值,反覆以上步驟,直到i==j
爲止,則i
的位置就是x
的正確排序位置,這時,i
下標的左邊全部元素都小於x
,i
下標右邊全部元素都大於x
,分別再遞歸求解。 - 代碼
int partition(int A[], int left, int right)
{
int x = A[left];
int i = left, j = right;
while (i != j)
{
while (i<j && A[j]>= x)
j--;
if (i < j)
A[i] = A[j];
while (i < j && A[i] <= x)
i++;
if(i <j)
A[j] = A[i];
}
A[i] = x;
return i;
}
void quickSort(int A[], int left, int right)
{
if (left < right)
{
int mid = partition(A, left, right);
quickSort(A, left, mid - 1);
quickSort(A, mid + 1, right);
}
}
7.5 堆排序(選擇排序)
堆排序相關知識請看4.11 堆排序(遞增)
7.6 歸併排序(分治思維)
- 思路:
利用分治的思維,將n
個元素的序列分解成n/2
個元素的兩個子序列,並遞歸分解,當待排序的序列長度爲1
時,遞歸開始回升。利用一個函數MERGE(A,p,q,r)
來完成合並,其中A
爲一個數組,p,q
和r
是數組下標,滿足p<=q<r
,該過程假設子數組A[p,q]
和A[p+1,r]
都已經排好序,它合併兩個子數組形成單一的已經排好的數組來代替A[p,r]
。 - 代碼
void merge(int A[], int left, int mid, int right)
{
//從小到大排序
//兩個待合併的數組A[left. mid]和A[left+1,right]
int nl = mid - left + 1;
int nr = right - mid;
vector<int> L;
vector<int> R;
for (int i = left; i <= mid; i++)
{
L.push_back(A[i]);
}
for (int j = mid+1; j <= right; j++)
{
R.push_back(A[j]);
}
int i = 0;
int j = 0;
int k = left;
while (i < L.size() && j < R.size())
{
if (L[i] < R[j])
A[k++] = L[i++];
else
A[k++] = R[j++];
}
while (i < L.size()) A[k++] = L[i++];
while (j < R.size()) A[k++] = R[j++];
}
void mergeSort(int A[], int left, int right)
{
if (left < right)
{
int mid = (left + right) / 2;
mergeSort(A, left, mid);
mergeSort(A, mid + 1, right);
merge(A, left, mid, right);
}
}
7.7 基數排序
- 代碼
int maxbit(int data[], int n) //輔助函數,求數據的最大位數
{
int maxData = data[0]; ///< 最大數
/// 先求出最大數,再求其位數,這樣有原先依次每個數判斷其位數,稍微優化點。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
}
void radixsort(int data[], int n) //基數排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //計數器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //進行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空計數器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //統計每個桶中的記錄數
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //將tmp中的位置依次分配給每個桶
for(j = n - 1; j >= 0; j--) //將所有桶中記錄依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //將臨時數組的內容複製到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}