[備戰軟考]資料結構與演算法基礎
:
資料結構與演算法基礎
線性表
1.順序表
順序的儲存結構,元素在記憶體中以順序儲存。記憶體中佔用連續的一個區域。
順序表的刪除
把要刪除的元素後面每個元素向前移動一位順序表的插入
把要插入的位置後面的(包括自己)所有元素向後移動一位,再把要插入的元素放入該位置。
2.連結串列
離散的儲存結構,各個點的儲存空間是離散的,通過指標聯絡起來,從而成為一個整體的連結串列。
單鏈表
從第一個元素開始指向下一個元素,最後一個元素指向NULL迴圈連結串列
最後一個元素指向頭雙鏈表
兩個指標域,從兩個方向連線
連結串列的操作
- 單鏈表的結點刪除
前驅指向後繼
Node* a1=(Node*)malloc(sizeof(Node));
Node* a2=(Node*)malloc(sizeof(Node));
Node* a3=(Node*)malloc(sizeof(Node));
a1->next=a2;
a2->next=a3;
a3->next=NULL;
cout<<a1->next<<endl;//未刪除時
//刪除a2這個結點
//關鍵步驟:將前一個元素的結點的next指向下一個節點
a1->next=a2->next;
//將刪除的結點的記憶體釋放
free(a2);
cout<<a1->next<<endl;
- 單鏈表的結點插入
①將新的結點指向後一個元素
②前一個結點指向要插入的結點
(順序不能顛倒,否則下一個結點的地址會找不到!)
Node* a1=(Node*)malloc(sizeof(Node));
Node* a2=(Node*)malloc(sizeof(Node));
a1->next=a2;
a2->next=NULL;
//cout<<a1->next<<' '<<a2<<endl;
// 插入x這個結點至a1與a2中間
Node* x=(Node*)malloc(sizeof(Node));
x->next=a1->next;//第一步
a1->next=x;
//cout<<a1->next<<' '<<x<<endl;
- 雙鏈表的結點插入和刪除
也是參照單鏈表的方法,做兩次操作而已。但是都要先進行完第一步,再進行第二步。
3.順序表與連結串列的比較
- 空間效能
專案 | 順序儲存 | 鏈式儲存 |
---|---|---|
儲存密度 | =1,更優 | <1 |
容量分配 | 事先確定 | 動態改變,更優 |
- 時間效能
專案 | 順序儲存 | 鏈式儲存 |
---|---|---|
查詢運算 | O(n/2) | O(n/2) |
讀運算 | O(1),更優 | O([n+1]/2),最好1,最壞n |
插入運算 | O(n/2),最好0,最壞n | O(1),更優 |
刪除運算 | O([n-1]/2) | O(1),更優 |
4.棧
先進後出,只能對棧頂進行操作。可以用順序表和連結串列實現。
5.佇列
先進先出,只能從隊尾插入,對頭讀取。
迴圈佇列
頭指標:head 尾指標:tail
如果沒有任何元素,head=tail,如果有元素入隊,tail向後移一位
如果在最後一個位置也插入了元素,那麼tail又會回到head的位置。
為了避免佇列空和佇列滿是一個狀態,將最後一個元素的位置捨棄不用。
樹和二叉樹
1.基本概念
結點的度:與下一層有幾個結點相關聯,它的度就是多少
樹的度:整個樹中度數最大的結點的度是多少,樹的度就是多少
葉子結點:度為0的結點
分支結點:除了葉子結點的所有結點都是分支結點(下一層有分支)
內部結點:分支結點中除了根結點的所有結點都是內部結點(中間層的結點)
父結點
子節點
兄弟結點
層次
公式:所有結點的度之和+1=結點總個數
2.樹的遍歷
前序遍歷:先訪問根節點,再依次訪問子結點(訪問完了一個子結點在訪問後一個子結點)
1 2 5 6 7 3 4 8 9 10
後序遍歷:先訪問子結點,再訪問根結點
5 6 7 2 3 9 10 8 4 1
層次遍歷:一層一層地訪問
1 2 3 4 5 6 7 8 9 10
3.二叉樹
每個結點最多隻能有兩個子結點,分為左子結點和右子結點。
滿二叉樹:二叉樹的每層都是滿的(完整金字塔形狀)
完全二叉樹:對於n層的二叉樹,其n-1層是滿二叉樹,第n層的結點從左到右連續排列
4.二叉樹的遍歷
與樹的遍歷是一樣的,就是多了一種中序遍歷
- 中序遍歷:先訪問左子結點,再訪問根節點,再訪問右子結點
5.查詢二叉樹(二叉排序樹)
空樹或滿足以下遞迴條件:
查詢樹的左右子樹各是一顆查詢樹
若左子樹非空,則左子樹上的各個結點的值均小於根節點的值
- 若右子樹非空,則左子樹上的各個結點的值均大於於根節點的值
基本操作
查詢:比較當前結點的值與鍵值,若鍵值小,則進入左子結點,若鍵值大,則進入右子結點
插入結點:
- 如果相同鍵值的結點已經在查詢二叉樹中,則不再插入
- 如果查詢二叉樹為空樹,則以新結點為查詢二叉樹
- 比較插入結點的鍵值與插入後的父節點的鍵值,就能確定新結點是父節點的左子結點還是右子結點,並插入
- 刪除操作
- 若刪除的結點p是葉子結點,則直接刪除
- 若p只有一個子結點,則將這個子結點與待刪除的結點的父節點直接連線,然後刪除節點p
- 若p有兩個子結點,在左子樹上,用中序遍歷找到關鍵值最大的結點s,用s的值代替p的值,然後刪除結點s,結點s必須滿足上面兩種情況之一
6.最優二叉樹(哈夫曼樹)
基本概念
- 樹的路徑長度:到達每個葉子結點所需要的長度之和
- 權:人為定義的每個結點的值
- 帶權路徑長度:路徑長度*該結點的權值
- 樹的帶權路徑長度(樹的代價):每個葉子結點的帶權路徑長度之和
構造哈夫曼樹
①把每個權值作為根節點,構造成樹
②選擇兩顆根節點最小的樹作為子樹合成一顆新的樹,根節點的值為兩個根節點值的和
③重複②,直到只剩一棵樹為止
7.線索二叉樹
- 表示
[Lbit][Lchild][Data][Rchild][Rbit]
標誌域規定:
Lbit=0,Lchild是通常的指標
Lbit=1,Lchild是線索(指向前驅)
Rbit=0,Rchild是通常的指標
Rbit=1,Rchild是線索(指向後繼)
將二叉樹轉化為線索二叉樹
對於每個空餘的左右指標,都用線索替代,左指標指向前驅,右指標指向後繼
前序:A B D E H C F G I
中序:D B H E A F C G I
後序:D H E B F I G C A
8.平衡二叉樹
對某個數列構造排序二叉樹,可以構造出多顆形式不同的排序二叉樹
- 定義:樹中任一結點的左、右子樹的深度相差不超過1
平衡樹調整
- LL型平衡旋轉(單向右旋平衡處理)
- RR型平衡旋轉(單向左旋平衡處理)
- LR型平衡旋轉(雙向旋轉,先左後右)
- RL型平衡旋轉(雙向旋轉,先右後左)
圖
1.基本概念
圖的構成:
圖由兩個集合:V和E所構成,V是非空點集,E是邊集,圖 G=(V,E)無向圖和有向圖:
邊是單向的是有向圖,雙向的就是無向圖頂點的度
無向圖:有幾條邊相連度就為幾
有向圖:分為入度和出度子圖
完全圖
無向圖中每對頂點都有一條邊相連,有向圖中每對頂點都有兩條有向邊相互連線路徑和迴路
連通圖
有向圖中,任意兩點都有路徑到達
無向圖中,沒有孤立點的圖強連通
有向圖中,任意兩點作為起點和終點都有路徑到達則為強連通。如果只能確保單向連通,則是弱連通。連通分量
圖的一個子圖是連通圖,那麼這個子圖就是一個連通分量網路
每一條邊都有一個權值
2.圖的儲存
(此部分圖片來自劉偉老師)
鄰接矩陣
鄰接表
又叫鄰接連結串列
3.圖的遍歷
深度優先(DFS)
- 首先訪問一個未訪問的節點V
- 依次從V出發搜尋V的每個鄰接點W
- 若W未訪問過,則從該點出發繼續深度優先遍歷
#include <iostream>
#include <cstring>
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
const int maxn=100;
struct EDG
{
int u;
int v;
int w;
//初始化列表
EDG(int uu=0,int vv=0,int ww=0):u(uu),v(vv),w(ww){}
}e[maxn];
int first[maxn],nxt[maxn*2];
int vis[maxn];
int len=0;
void mk_edg(int u,int v,int w)//加入u到v權值為w的邊
{
e[++len]=EDG(u,v,w);
nxt[len]=first[u];
first[u]=len;
e[++len]=EDG(v,u,w);
nxt[len]=first[v];
first[v]=len;
}
//圖的深度優先遍歷(遞迴寫法)
void DFS(int v)
{
vis[v]=1;
cout<<v<<' ';
for(int i=first[v];i!=-1;i=nxt[i])
{
if(!vis[e[i].v])
{
DFS(e[i].v);
}
}
}
int main()
{
mem(first,-1),mem(nxt,-1),mem(vis,0);
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++)
{
int u,v,w;
cin>>u>>v>>w;
mk_edg(u,v,w);
mk_edg(v,u,w);
}
for(int i=0;i<n;i++)
{
if(!vis[i])
DFS(i);
}
return 0;
}
廣度優先(BFS)
- 首先訪問一個未訪問的頂點V
- 然後訪問與頂點V鄰接的全部未訪問頂點W、X、Y……
- 然後再依次訪問W、X、Y鄰接的未訪問頂點
4.最小生成樹
(此部分程式碼來自劉偉老師)
Prim演算法
思想:
(1) 任意選定一點s,設集合S={s}
(2) 從不在集合S的點中選出一個點j使得其與S內的某點i的距離最短,則(i,j)就是生成樹上的一條邊,同時將j點加入S
(3) 轉到(2)繼續進行,直至所有點都己加入S集合
#include<iostream>
using namespace std;
#define MAXN 2001
#define INF 1000000
int n, m;
int G[MAXN][MAXN]; //儲存圖
void init(){
for(int i = 0 ; i < n ; i++){
for(int j = 0 ; j < n ; j++)
G[i][j] = INF; //初始化圖中兩點間距離為無窮大
}
}
void prim(){
int closeset[n], //記錄不在S中的頂點在S中的最近鄰接點
lowcost[n], //記錄不在S中的頂點到S的最短距離,即到最近鄰接點的權值
used[n]; //標記頂點是否被訪問,訪問過的頂點標記為1
for (int i = 0; i < n; i++)
{
//初始化,S中只有第1個點(0)
lowcost[i] = G[0][i]; //獲取其他頂點到第1個點(0)的距離,不直接相鄰的頂點距離為無窮大
closeset[i] = 0; //初始情況下所有點的最近鄰接點都為第1個點(0)
used[i] = 0; //初始情況下所有點都沒有被訪問過
}
used[0] = 1; //訪問第1個點(0),將第1個點加到S中
//每一次迴圈找出一個到S距離最近的頂點
for (int i = 1; i < n; i++)
{
int j = 0;
//每一次迴圈計算所有沒有使用的頂點到當前S的距離,得到在沒有使用的頂點中到S的最短距離以及頂點號
for (int k = 0; k < n; k++)
if ((!used[k]) && (lowcost[k] < lowcost[j])) j = k; //如果頂點k沒有被使用,且到S的距離小於j到S的距離,將k賦給j
printf("%d %d %d\n",closeset[j] + 1, j + 1, lowcost[j]); //輸出S中與j最近鄰點,j,以及它們之間的距離
used[j] = 1; //將j增加到S中
//每一次迴圈用於在j加入S後,重新計算不在S中的頂點到S的距離
//主要是修改與j相鄰的邊到S的距離,修改lowcost和closeset
for (int k = 0; k < n; k++)
{
if ((!used[k]) && (G[j][k] < lowcost[k])) //鬆弛操作,如果k沒有被使用,且k到j的距離比原來k到S的距離小
{
lowcost[k] = G[j][k]; //將k到j的距離作為新的k到S之間的距離
closeset[k] = j; //將j作為k在S中的最近鄰點
}
}
}
}
int main(){
int a , b , w;
scanf("%d%d" , &n , &m);
init();
for(int i = 0 ; i < m ; i++){
scanf("%d%d%d" , &a , &b , &w);
if(G[a-1][b-1] > w)
G[a-1][b-1] = G[b-1][a-1] = w; //無向圖賦權值
}
prim();
system("pause");
return 0;
}
Kruskal演算法
思想:
(1) 將邊按權值從小到大排序後逐個判斷,如果當前的邊加入以後不會產生環,那麼就把當前邊作為生成樹的一條邊
(2) 最終得到的結果就是最小生成樹
#include <iostream>
#include <algorithm>
using namespace std;
/* 定義邊(x,y),權為w */
struct edge
{
int x, y;
int w;
};
const int MAX = 26;
edge e[MAX * MAX];
int rank[MAX];/* rank[x]表示x的秩 */
int father[MAX];/* father[x]表示x的父節點 */
int sum; /*儲存最小生成樹的總權重 */
/* 比較函式,按權值非降序排序 */
bool cmp(const edge a, const edge b)
{
return a.w < b.w;
}
/* 初始化集合 */
void make_set(int x)
{
father[x] = x;
rank[x] = 0;
}
/* 查詢x元素所在的集合,回溯時壓縮路徑 */
int find_set(int x)
{
if (x != father[x])
{
father[x] = find_set(father[x]);
}
return father[x];
}
/* 合併x,y所在的集合 */
int union_set(int x, int y, int w)
{
if (x == y) return 0;
if (rank[x] > rank[y])
{
father[y] = x;
}
else
{
if (rank[x] == rank[y])
{
rank[y]++;
}
father[x] = y;
}
sum += w; //記錄權重
return 1;
}
int main()
{
int i, j, k, m, n, t;
char ch;
while(cin >> m && m != 0)
{
k = 0;
for (i = 0; i < m; i++) make_set(i); //初始化集合,m為頂點個數
//對後m-1進行逐行處理
for (i = 0; i < m - 1; i++)
{
cin >> ch >> n; //獲取字元(頂點)
for (j = 0; j < n; j++)
{
cin >> ch >> e[k].w; //獲取權重
e[k].x = i;
e[k].y = ch - 'A';
k++;
}
}
sort(e, e + k, cmp); //STL中的函式,直接對陣列進行排序
sum = 0;
for (i = 0; i < k; i++)
{
int result = union_set(find_set(e[i].x), find_set(e[i].y), e[i].w);
if(result) cout<< e[i].x + 1<< "," << e[i].y + 1 <<endl;
}
cout << sum << endl;
}
system("pause");
return 0;
}
5.拓撲排序
AOV網
拓撲排序
(1) 將所有入度為0的點加入佇列
(2) 每次取出隊首頂點
(3) 刪除其連出的邊,檢查是否有新的入度為0的頂點,有則加入佇列
(4) 重複(2)直到佇列為空
6.關鍵路徑
- AOE網
在AOV網中把邊加上權值就變成了AOE網
概念:
- 頂點 j 事件的最早發生時間,即從源點到頂點 j 的最長路徑長度,記作Ve( j );
- 活動ai的最早發生時間:Ve( j )是以頂點為 j 為起點的出邊所表示的活動ai的最早開始時間,記作e( i )
- 頂點 j 事件的最遲發生時間:即在不推遲整個工程完成的前提下,事件 j 允許最遲的發生時間,記作Vl( j );
- 活動ai的最遲發生時間:Vl( j ) - (ai所需的時間),就是活動ai的最遲開始時間,其中j是活動ai的終點,記作l(i);
排序演算法
1.插入排序
直接插入排序:
- 每一步把當前的數插入到已經有序的序列中
Shell排序:也稱縮小增量排序
- 根據步長d,把相距間隔為d的元素分到一組,在內部進行直接插入排序,然後步長減半,重複這一步操作,直到步長d=1為止
2.選擇排序
簡單選擇排序:
- 每一步查詢剩餘序列中最小的元素,然後將該元素放到已序序列的末尾
堆排序:
- 還沒搞明白
3.交換排序
氣泡排序:
- 從後往前每一步對比相鄰的兩個元素,如果後面的元素小,則交換
快速排序:
- 運用分治思想。每次選擇第一個元素作為基準,將比它小的元素放前面,比它大的放後面,接著在這兩個子區間繼續進行這一操作。
4.歸併排序
- 首先把元素兩個一組分組,使每個組內都有序,接下來在把每兩組合並,重複這一操作直到只剩一組。
5.基數排序
- 根據元素的每一位來排序,高位的優先順序比低位的高
雜湊表
Hash表示一種十分實用的查詢技術,具有極高的查詢效率
1.Hash函式的構造
沒有特定的要求,所以方法很多,只要能儘量避免衝突,就叫好的Hash函式,要根據實際情況來構造合理的Hash函式
直接定址法
H(key)=key 或 H(key) = a*key+b除餘法
以關鍵碼除以表元素總數後得到的餘數為地址基數轉換法
將關鍵碼看作是某個基數制上的整數,然後將其轉換為另一基數制上的數平方取中法
取關鍵碼的平方值,根據表長度取中間的幾位數作為雜湊函式值摺疊法
將關鍵碼分成多段,左邊的段向右折,右邊的段向左折,然後疊加移位法
將關鍵碼分為多段,左邊的段右移,右邊的段左移,然後疊加隨機數法
選擇一個隨機函式,取關鍵碼的隨機函式值
2.衝突處理方法
開放地址法
- 線性探查法:衝突後直接向下線性地址找一個新的空間存放
- 雙雜湊函式法:用兩個雜湊函式來解決衝突
拉鍊法
將散列表的每個結點增加一個指標欄位,用於連結同義詞的字表
查詢演算法
1.順序查詢
從一端開始逐個對比當前結點和關鍵字是否相等
2.二分查詢
(要求待查序列為有序表)
每次對比中點和關鍵字是否相等,若相等則找到。若關鍵字大,則在右邊的區間繼續這一操作,否則在左邊的區間繼續這一操作
3.分塊查詢
用索引表記錄塊的最大關鍵字和起始地址,然後查詢的時候只要找到關鍵字所在的塊,然後在對應的塊中查詢就可以了