1. 程式人生 > >[備戰軟考]資料結構與演算法基礎

[備戰軟考]資料結構與演算法基礎

資料結構與演算法基礎

線性表

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)=keyH(key) = a*key+b

  • 除餘法
    以關鍵碼除以表元素總數後得到的餘數為地址

  • 基數轉換法
    將關鍵碼看作是某個基數制上的整數,然後將其轉換為另一基數制上的數

  • 平方取中法
    取關鍵碼的平方值,根據表長度取中間的幾位數作為雜湊函式值

  • 摺疊法
    將關鍵碼分成多段,左邊的段向右折,右邊的段向左折,然後疊加

  • 移位法
    將關鍵碼分為多段,左邊的段右移,右邊的段左移,然後疊加

  • 隨機數法
    選擇一個隨機函式,取關鍵碼的隨機函式值

2.衝突處理方法

開放地址法

  • 線性探查法:衝突後直接向下線性地址找一個新的空間存放
  • 雙雜湊函式法:用兩個雜湊函式來解決衝突

拉鍊法

將散列表的每個結點增加一個指標欄位,用於連結同義詞的字表

查詢演算法

1.順序查詢

從一端開始逐個對比當前結點和關鍵字是否相等

2.二分查詢

要求待查序列為有序表
每次對比中點和關鍵字是否相等,若相等則找到。若關鍵字大,則在右邊的區間繼續這一操作,否則在左邊的區間繼續這一操作

3.分塊查詢

用索引表記錄塊的最大關鍵字和起始地址,然後查詢的時候只要找到關鍵字所在的塊,然後在對應的塊中查詢就可以了