1. 程式人生 > >資料結構(非線性表)

資料結構(非線性表)

非線性結構-樹

實驗簡介

前面兩章我們講解了資料結構中的線性結構--線性表、棧和佇列,這章開始以及下一章我們將講解非線性結構樹和圖。

一、樹

什麼是樹呢?樹很好地反應了一種層次結構,例如下圖,這就是一種樹形結構,它有很多結點組成,最上面的實驗樓課程結點稱為樹的,結點擁有的直接子節點數稱為結點的度,度為0的結點稱為葉子,例如C語言、評估課這些結點,而樹的度是所有結點的度中的最大值,這顆樹的度就是3,一個結點的直接子結點稱為它的孩子,專案課結點的孩子就是製作Markdown預覽器結點,相應地專案課結點就是製作Markdown預覽器結點的雙親,相同雙親的孩子結點互稱為兄弟,例如C語言結點和Linux入門結點,一個結點的祖先

是從根到該結點所經過的所有結點,C語言結點的祖先就是基礎課和實驗樓課程結點,一個結點下的所有結點稱為該結點的子孫,例如實驗樓課程下的所有結點都是它的子孫。樹有層次之分,根記為第一層,依次類推,例如這棵樹的最大層次就是3,也稱為該樹的深度,雙親在同一層的結點互稱為堂兄弟,例如Linux入門結點和製作Markdown預覽器結點。

二、二叉樹

上面介紹了樹,接下來我們介紹一種很常用的樹結構--二叉樹,它的特點是一個結點的直接子節點最多隻能有兩個,並且有左右之分。在二叉樹中有種常見的稱為完全二叉樹的結構,它的特點是除最後一層外每一層的結點數為2i-1,最後一層的結點數若不滿足2i-1

,那麼最後一層的結點是自左向右排列的,如下圖。

二叉樹也有順序儲存結構和鏈式儲存結構兩種,這裡我們就講下鏈式儲存結構的程式碼實現(主要操作):

#include <stdio.h>
#include <stdlib.h>

#define TRUE 1
#define FALSE 0
#define OVERFLOW -2
#define OK 1
#define ERROR 0

typedef int Status;
typedef int TElemType;

/*
 * 儲存結構
 */
typedef struct BiTNode
{
    TElemType data;    //資料
struct BiTNode *lchild, *rchild; }BiTNode, *BiTree; /* * 建立二叉樹,輸入0表示建立空樹 */ Status CreateBiTree(BiTree *T) { TElemType e; scanf("%d", &e); if (e == 0) { *T = NULL; } else { *T = (BiTree) malloc(sizeof(BiTNode)); if (!T) { exit(OVERFLOW); } (*T)->data = e; CreateBiTree(&(*T)->lchild); //建立左子樹 CreateBiTree(&(*T)->rchild); //建立右子樹 } return OK; } /* * 訪問元素 */ void visit(TElemType e) { printf("%d ", e); } /* * 先序遍歷二叉樹:指先訪問根,然後訪問孩子的遍歷方式 */ Status PreOrderTraverse(BiTree T, void (*visit)(TElemType)) { if (T) { visit(T->data); PreOrderTraverse(T->lchild, visit); PreOrderTraverse(T->rchild, visit); } } /* * 中序遍歷二叉樹:指先訪問左(右)孩子,然後訪問根,最後訪問右(左)孩子的遍歷方式 */ Status InOrderTraverse(BiTree T, void (*visit)(TElemType)) { if (T) { InOrderTraverse(T->lchild, visit); visit(T->data); InOrderTraverse(T->rchild, visit); } } /* * 後序遍歷二叉樹:指先訪問孩子,然後訪問根的遍歷方式 */ Status PostOrderTraverse(BiTree T, void (*visit)(TElemType)) { if (T) { PostOrderTraverse(T->lchild, visit); PostOrderTraverse(T->rchild, visit); visit(T->data); } } int main() { BiTree T; printf("建立樹,輸入0為空樹:\n"); CreateBiTree(&T); printf("先序遍歷:"); PreOrderTraverse(T, *visit); printf("\n中序遍歷:"); InOrderTraverse(T, *visit); printf("\n後序遍歷:"); PostOrderTraverse(T, *visit); printf("\n"); return 0; }

上面我們講了二叉樹的一些主要操作,其實它的操作遠不止這些,例如你可以試試把遍歷改為非遞迴實現、求樹的深度等等一些操作。除了上面實現的基本二叉樹之外,還有一種線索二叉樹,它其實就是用結點空的指標域來指向它的前驅或者後繼結點,不浪費空的指標域,如果你想深入瞭解,可以查查資料。

三、堆

堆是一種經過排序的完全二叉樹,其中任一非葉子節點的值均不大於(或不小於)其左孩子和右孩子節點的值。

最大堆和最小堆是二叉堆的兩種形式。

最大堆:根結點的鍵值是所有堆結點鍵值中最大者。

最小堆:根結點的鍵值是所有堆結點鍵值中最小者。

而最大-最小堆集結了最大堆和最小堆的優點,這也是其名字的由來。

最大-最小堆是最大層和最小層交替出現的二叉樹,即最大層結點的兒子屬於最小層,最小層結點的兒子屬於最大層。

以最大(小)層結點為根結點的子樹保有最大(小)堆性質:根結點的鍵值為該子樹結點鍵值中最大(小)項。

最小堆

最大堆

四、二叉排序樹

二叉排序樹又稱二叉查詢樹,亦稱二叉搜尋樹,如下圖所示,它主要用於查詢。 它或者是一棵空樹;或者是具有下列性質的二叉樹:

(1)若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;

(2)若右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 

(3)左、右子樹也分別為二叉排序樹;

五、平衡二叉樹

平衡二叉樹又被稱為AVL樹,且具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹,如下圖,由它可以生成平衡二叉搜尋樹,查詢效率會更高。構造與調整方法平衡二叉樹的常用演算法有紅黑樹、AVL、Treap等。最小二叉平衡樹的節點的公式如下F(n)=F(n-1)+F(n-2)+1這個類似於一個遞迴的數列,可以參考Fibonacci數列,1是根節點,F(n-1)是左子樹的節點數量,F(n-2)是右子樹的節點數量。

六、哈夫曼樹

哈夫曼樹也稱最優二叉樹,它是帶權路徑長度最小的二叉樹。下面我就通過一個例子讓大家快速地明白,相信大家都看過抗日電視劇,打仗的時候,前線要與後方指揮部取得聯絡通常都會使用電報,那麼電報編碼後的長度當然是越短越好,但同時翻譯電報時又不能造成歧義,這時候就可以使用哈夫曼樹來編碼,那麼怎麼實現呢?

哈夫曼樹的構造步驟如下:

假設有n個權值,則構造出的哈夫曼樹有n個葉子結點。 n個權值分別設為 w1、w2、…、wn,則哈夫曼樹的構造規則為:

(1) 將w1、w2、…、wn看成是有n 棵樹的集合(每棵樹僅有一個結點);

(2) 在集合中選出兩個根結點的權值最小的樹合併,作為一棵新樹的左、右子樹,且新樹的根結點權值為其左、右子樹根結點權值之和;

(3)從集合中刪除選取的兩棵樹,並將新樹加入集合;

(4)重複(2)、(3)步,直到集合中只剩一棵樹為止,該樹即為所求得的哈夫曼樹。

比如需要傳送“goodgoodstudy”,我們先計算每個字母出現的次數即權值,g:2、o:4、d:3、s:1、t:1、u:1、y:1,然後通過哈夫曼樹的構造規則構造出哈夫曼樹,如下圖。

通過構造哈夫曼樹我們就能得到每個字母的編碼,g:010、o:00、d:10、s:0110、t:0111、u:110、y:111,這就能使編碼總長度最小,此種編碼就是著名的哈夫曼編碼。

七、小結

本章我們講了非線性結構樹、二叉樹以及哈夫曼樹(最優二叉樹),樹結構體現的是一種層次結構,二叉樹結點的直接子節點最多隻能有兩個,可以解決表示式求值等問題。堆是一種經過排序的完全二叉樹,其中任一非葉子節點的值均不大於(或不小於)其左孩子和右孩子節點的值,堆有最大堆、最小堆和最大-最小堆。二叉搜尋樹和平衡二叉樹主要用於查詢,還有B-樹和B+樹也是用於查詢,它們主要應用於檔案系統。哈夫曼樹是一種帶權路徑長度最小的樹,哈夫曼編碼就是由它而得名


非線性結構-圖

實驗簡介

前面已經講了幾種線性結構和樹形結構,本章來講解比它們更為複雜的圖結構。線上性結構中,元素之間是一種線性關係,只有一個直接前驅和一個直接後繼,而樹結構體現的是一種層次關係,在圖中每個元素之間都可能是有關聯的。

1.什麼是圖

下面就通過一個例子來讓大家快速地知道什麼是圖,如下圖所示,G1是有向圖,G2是無向圖,每個資料元素稱為頂點,在有向圖中,從V1到V3稱為一條,V3到V1為另一條弧,V1稱為弧尾,V3稱為弧頭,在無向圖中,從V1到V3稱為一條。有n個頂點,1/2n(n-1)條邊的無向圖稱為完全圖,有n(n-1)條弧有向圖稱為有向完全圖,有很少條邊或圖稱為稀疏圖,反之稱為稠密圖。在G2無向圖中,類似V3與V1、V2和V4之間有邊的互稱為鄰接點,與頂點相關聯的邊數稱為頂點的,例如V3頂點的度為3,而在G1有向圖中,頂點的是頂點的出度和入度之和,以頂點為頭的弧的數目稱為入度,為尾的弧的數目稱為出度,例如V1頂點的出度為2,入度為1,它的度為1+2=3。從一個頂點到另一個頂點的頂點序列稱為路徑,在有向圖中,路徑是有方向的,路徑上邊或弧的數目稱為路徑的長度,如果一條路徑中的起始頂點跟結束結點相同,那麼稱這個路徑為環或迴路,不出現重複頂點的路徑稱為簡單路徑。無向圖中,如果一個頂點到另一個頂點有路徑,那麼它們就是連通的,如果圖中的任意兩個頂點都是連通的,那麼這個圖就是連通圖,無向圖中的極大連通子圖稱為連通分量,如果是有向圖中的任意一對頂點都有路徑,那麼這個就是強連通圖,相應的它的極大連通子圖就稱為強連通分量。一個連通圖的一個極小連通子圖,它包含所有頂點,但足以構成一棵樹的n-1條邊,加一條邊必定會形成環,這個就稱為*生成樹

2. 圖的表示和實現

表示圖通常有四種方法--陣列表示法、鄰接表、十字連結串列和鄰接多重表。鄰接表是圖的一種鏈式儲存結構,十字連結串列是有向圖的另一種鏈式儲存結構,鄰接多重表是無向圖的另一種鏈式儲存結構。這裡主要講解一下鄰接表的表示和實現,鄰接表中有兩種結點,一種是頭結點,另一種是表結點,頭結點中儲存一個頂點的資料和指向連結串列中第一個結點,表結點中儲存當前頂點在圖中的位置和指向下一條邊或弧的結點,表頭結點用鏈式或順序結構方式儲存,如下圖所示就是上圖G2無向圖的鄰接表表示。

3. 圖的遍歷

通常圖的遍歷有兩種:深度優先搜尋和廣度優先搜尋。

深度優先搜尋是樹的先根遍歷的推廣,它的基本思想是:從圖G的某個頂點v0出發,訪問v0,然後選擇一個與v0相鄰且沒被訪問過的頂點vi訪問,再從vi出發選擇一個與vi相鄰且未被訪問的頂點vj進行訪問,依次繼續。如果當前被訪問過的頂點的所有鄰接頂點都已被訪問,則退回到已被訪問的頂點序列中最後一個擁有未被訪問的相鄰頂點的頂點w,從w出發按同樣的方法向前遍歷,直到圖中所有頂點都被訪問。

廣度優先搜尋是樹的按層次遍歷的推廣,它的基本思想是:首先訪問初始點vi,並將其標記為已訪問過,接著訪問vi的所有未被訪問過的鄰接點vi1,vi2,…, vin,並均標記已訪問過,然後再按照vi1,vi2,…, vin的次序,訪問每一個頂點的所有未被訪問過的鄰接點,並均標記為已訪問過,依次類推,直到圖中所有和初始點vi有路徑相通的頂點都被訪問過為止。

如下圖

深度優先搜尋:0->1->3->7->4->2->5->6

廣度優先搜尋:0->1->2->3->4->5->6->7

下面是鄰接表的建立和圖的遍歷的程式碼實現:

#include <stdio.h>
#include <stdlib.h>

#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define OVERFLOW -2
#define MAX_NUM 20

typedef int Status;
typedef int QElemType;
typedef char VexType;

/*
 * 鄰接表儲存結構
 */
typedef struct EdgeNode
{
    int adjvex;    //頂點的位置
    struct EdgeNode *next; //指向下一條邊的指標
}EdgeNode, *EdgeLink;

typedef struct VexNode
{
    VexType data;    //頂點資料
    EdgeNode *firstEdge;    //指向第一條依附該頂點的邊的指標
}VexNode, AdjList[MAX_NUM];

typedef struct
{
    AdjList adjList;
    int vexNum, edgeNum;    //頂點數和邊數
}ALGraph;

/*
 * 佇列儲存結構(用於圖的遍歷)
 */
typedef struct QNode
{
    QElemType data;    //結點資料
    struct QNode *next;    //指向下一個結點
}QNode, *QueuePtr;

typedef struct
{
    QueuePtr front;    //隊頭指標
    QueuePtr rear;    //隊尾指標
}LinkQueue;

/*
 * 初始化佇列
 */
Status InitQueue(LinkQueue *Q)
{
    Q->front = Q->rear = (QueuePtr) malloc(sizeof(QNode));
    if (!Q->front)
    {
        exit(OVERFLOW);
    }
    Q->front->next = NULL;
    return OK;
}

/*
 * 判斷佇列是否為空
 */
Status IsEmpty(LinkQueue Q)
{
    if (Q.front->next == NULL)
    {
        return TRUE;
    }
    else
    {
        return FALSE;
    }
}

/*
 * 入隊
 */
Status EnQueue(LinkQueue *Q, QElemType e)
{
    QueuePtr p = (QueuePtr) malloc(sizeof(QNode));
    if (!p)
    {
        exit(OVERFLOW);
    }
    p->data = e;
    p->next = NULL;
    Q->rear->next = p;
    Q->rear = p;
    return OK;
}

/*
 * 出隊
 */
Status DeQueue(LinkQueue *Q, QElemType *e)
{
    QueuePtr p;
    if (Q->front == Q->rear)
    {
        return ERROR;
    }
    p = Q->front->next;
    *e = p->data;
    Q->front->next = p->next;
    if (Q->rear == p)
    {
        Q->rear = Q->front;
    }
    free(p);
    return OK;
}

/*
 * 建立圖
 */
Status CreateGraph(ALGraph *G)
{
    int i, j, k;
    EdgeLink e;
    printf("請輸入頂點數目和邊數:\n");
    scanf("%d", &G->vexNum);
    scanf("%d", &G->edgeNum);
    getchar();
    printf("請輸入各頂點的資料:\n");
    for (i = 0; i < G->vexNum; i++)
    {
        scanf("%c",&G->adjList[i].data);
        if (G->adjList[i].data == '\n')
        {
            i--;
            continue;
        }
        G->adjList[i].firstEdge = NULL;
    }

    printf("請依次輸入邊(Vi,Vj)的頂點序號:\n");
    for (k = 0; k < G->edgeNum; k++)
    {
        scanf("%d", &i);
        scanf("%d", &j);
        e = (EdgeLink) malloc(sizeof(EdgeNode));
        e->adjvex = j;
        e->next = G->adjList[i].firstEdge;
        G->adjList[i].firstEdge = e;
        e = (EdgeLink) malloc(sizeof(EdgeNode));
        e->adjvex = i;
        e->next = G->adjList[j].firstEdge;
        G->adjList[j].firstEdge = e;
    }
    return OK;
}

int visited[MAX_NUM];    //用於記錄遍歷狀態

/*
 * 遞迴從第i個結點深度優先遍歷圖
 */
void DFS(ALGraph G, int i)
{
    EdgeLink p;
    visited[i] = TRUE;
    printf("%c ", G.adjList[i].data);
    p = G.adjList[i].firstEdge;
    while (p)
    {
        if (!visited[p->adjvex])
        {
            DFS(G, p->adjvex);
        }
        p = p->next;
    }
}

/*
 * 深度優先遍歷
 */
Status DFSTraverse(ALGraph G)
{
    int i;
    for (i = 0; i < MAX_NUM; i++)
    {
        visited[i] = FALSE;
    }
    for (i = 0; i < G.vexNum; i++)
    {
        if (!visited[i])
        {
            DFS(G, i);
        }
    }
    return OK;
}

/*
 * 廣度優先遍歷
 */
Status BFSTraverse(ALGraph G)
{
    int i;
    EdgeLink p;
    LinkQueue Q;
    InitQueue(&Q);
    for (i = 0; i < MAX_NUM; i++)
    {
        visited[i] = FALSE;
    }
    for (i = 0; i < G.vexNum; i++)
    {
        if (!visited[i])
        {
            visited[i] = TRUE;
            printf("%c ", G.adjList[i].data);
            EnQueue(&Q, i);
            while (!IsEmpty(Q))
            {
                DeQueue(&Q, &i);
                p = G.adjList[i].firstEdge;
                while (p)
                {
                    if (!visited[p->adjvex])
                    {
                        visited[p->adjvex] = TRUE;
                        printf("%c ", G.adjList[p->adjvex].data);
                        EnQueue(&Q, p->adjvex);
                    }
                    p = p->next;
                }
            }
        }
    }
    return OK;
}

int main()
{
    ALGraph G;
    CreateGraph(&G);
    printf("深度優先遍歷:");
    DFSTraverse(G);
    printf("\n廣度優先遍歷:");
    BFSTraverse(G);
    printf("\n");
}

4. 最小生成樹

一個有n個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的所有n個結點,並且有保持圖連通的最少的邊。最小生成樹可以用kruskal(克魯斯卡爾)演算法或Prim(普里姆)演算法求出。

應用:例如要在n個城市之間鋪設光纜,主要目標是要使這n個城市的任意兩個之間都可以通訊,但鋪設光纜的費用很高,且各個城市之間鋪設光纜的費用不同,因此另一個目標是要使鋪設光纜的總費用最低。這就需要找到帶權的最小生成樹。

參考自百度百科

5. 拓撲排序

拓撲排序簡單地說,就是在有向圖中,想訪問一個頂點需要先訪問它的所有前驅頂點。它的執行步驟為:

  1. 在有向圖中選一個沒有前驅的頂點輸出。
  2. 從圖中刪除該頂點和所有以它為尾的弧。 重複上述步驟直到所有頂點都輸出或者圖中不存在無前驅的頂點為止,後者說明圖中有環。

如上圖,它的拓撲序列就為:

Linux基礎入門->Vim編輯器->Git Community Book->HTML基礎入門->SQL基礎課程->MySQL參考手冊中文版->Python程式語言->Python Flask Web框架->Flask開發輕部落格

6. 最短路徑問題

最短路徑問題是圖論研究中的一個經典演算法問題,旨在尋找圖(由結點和路徑組成的)中兩結點之間的最短路徑。Dijkstra(迪傑斯特拉)演算法是典型的最短路徑路由演算法,用於計算一個節點到其他所有節點的最短路徑。主要特點是以起始點為中心向外層層擴充套件,直到擴充套件到終點為止。Dijkstra演算法能得出最短路徑的最優解,但由於它遍歷計算的節點很多,所以效率低。

其採用的是貪心法的演算法策略,大概過程為先建立兩個表,OPEN和CLOSE表,OPEN表儲存所有已生成而未考察的節點,CLOSED表中記錄已訪問過的節點,然後:

1. 訪問路網中距離起始點最近且沒有被檢查過的點,把這個點放入OPEN組中等待檢查。

2. 從OPEN表中找出距起始點最近的點,找出這個點的所有子節點,把這個點放到CLOSE表中。

3. 遍歷考察這個點的子節點。求出這些子節點距起始點的距離值,放子節點到OPEN表中。

4. 重複第2和第3步,直到OPEN表為空,或找到目標點。