經典資料結構和演算法回顧

lvyahui的部落格發表於2015-08-02

最近想回過頭來看看以前寫的一些程式碼,可嘆為何剛進大學的時候不知道要養成寫部落格的好習慣。現在好多東西都沒有做記錄,後面也沒再遇到相同的問題,忘的都差不多了。只能勉強整理了下面寫的一些程式碼,這些程式碼有的有參考別人的程式碼,但都是自己曾經一點點敲的,掛出來,雖然很基礎,但希望能對別人有幫助。

連結串列

連結串列是一種非常基本的資料結構,被廣泛的用在各種語言的集合框架中。

首先連結串列是一張表,只不過連結串列中的元素在記憶體中不一定相鄰,並且每個元素都可帶有指向另一個元素的指標。

連結串列有,單項鍊表,雙向連結串列,迴圈連結串列等。

單項鍊表的資料結構

如下

 typedef struct NODE{
     struct NODE * link;
     int value;
 } Node;

對連結串列的操作

主要有增刪查

Node * create(){
    Node * head,* p,* tail;
    //  這裡建立不帶頭節點的連結串列
    head=NULL;
    do
    {
        p=(Node*)malloc(LEN);
        scanf("%ld",&p->value);

        if(p->value ==0) break;
        //  第一次插入
        if(head==NULL)
            head=p;
        else
            tail->link=p;
        tail=p;
    }
    while(1);
    tail->link=NULL;
    return head;
}

int delet(Node **linkp,int del_value){
    register Node * current;
    Node * m_del;

    //尋找正確的刪除位置,方法是按順序訪問連結串列,直到到達等於的節點
    while((current = *linkp)!=NULL  &&  current->value != del_value)
    {    
        linkp = &current->link;
    }

    if(NULL==current)
        return FALSE;
    else
    {
        //把該節點刪除,返回TRUE
        m_del=current->link;
        free(current);
        *linkp=m_del;
    }
    return TRUE;
}
//需要形參為連結串列頭指標的地址和要插入的值
int insert(Node **linkp,int new_value){
    register Node * current;
    Node * m_new;

    //尋找真確的插入位置,方法是按順序訪問連結串列,直到到達其值大於或等於新插入值的節點
    while((current = *linkp)!=NULL  &&  current->value < new_value)
    {    
        linkp = &current->link;
    }
    //為新節點分配記憶體,並把新值存到新節點中,如果分配失敗,返回FALSE
    m_new =(Node*)malloc(LEN);
    if(NULL==m_new)
        return FALSE;
    m_new->value = new_value;
    //把新節點放入連結串列,返回TRUE

    m_new->link = current;
    *linkp=m_new;

    return TRUE;
}

僅僅只需要將尾指標指向頭節點,就可以構成單項迴圈連結串列,即tail->link=head;,有的時候,可能需要將連結串列逆置,當然,如果需要逆置,最好一開始就做雙向連結串列。

Node * reverse(Node * head){
    Node * p,*q;
    q= head;
    p = head->link;
    head = NULL;
    while(p)
    {    
        //  接到新連結串列裡面去
        q->link = head; 
        head  = q;
        //  繼續遍歷原來的連結串列
        q = p;
        p = p->link;
    }
    q->link = head; 
    head  = q;
    return  head;
}

刪除連結串列中所有值為x的節點,以及清除連結串列中重複的節點

//    功能:    刪除連結串列中所有值為x的節點
//    形參:    1、若不帶頭結點,便是連結串列頭指標的地址,即&head
//            2、若帶頭結點,便是連結串列頭節點的next域的地址,即&head->next
//    形參:    為連結串列頭指標的地址和要刪除的值
void del_link(Node ** plink,int x){
    register Node * current;
    while((current = *plink)!=NULL)
    {
        // 處理連續出現x的情況
        while(current && current->data == x){
            //  保留指向下一個節點的指標
            Node * temp = current;
            * plink = current = current->next;
            //  刪除當前節點
            free(temp);
        }

        //向下遍歷連結串列
        if (current)
        {
            plink = &current->next;
        }
    }
}
//    功能:    刪除連結串列中重複多餘的節點
//    形參:    1、若不帶頭結點,便是連結串列頭指標的地址,即&head
//            2、若帶頭結點,便是連結串列頭節點的next域的地址,即&head->next
void del_linkAll(Node ** plink){
    register Node * current;
    while((current = *plink) != NULL){
        //注意,這裡取指向下一個元素的指標的地址,這樣刪除是會保留這一個節點
        del_link(&current->next,current->data);
        plink = &current->next;
    }
}

對於雙向連結串列,也就是在節點中再新增一個節點,讓它與另一個指標指向的方向相反。當然,當節點有了兩個節點之後,就可以構成更復雜的比如樹圖等複雜結構了,雙向連結串列可像如下定義

#ifndef __LINKLISTEX_H__
#define __LINKLISTEX_H__
#include <string>
using std::string;
//================雙向連結串列的定義===============
template<class T>
class DulLinkList
{
private:
    typedef struct DulNode{
        struct DulNode * prior;
        T    data;
        struct DulNode * next;
    }DulNode;
    DulNode * frist;
    void Init();
    void Del(DulNode * delNode);
public:
    DulLinkList();
    ~DulLinkList();
    void AddElem(const T &  data);
    void DelElem(const T & data);
    string ToString()const;
protected:
};
#endif//__LINKLISTEX_H__

對雙向連結串列的操作也無外乎增刪改

#include "LinkListEx.h"
#include <iostream>
using namespace  std;

template<class T>
DulLinkList<T>::DulLinkList(){
    Init();
}

template<class T>
void DulLinkList<T>::Init(){
    // 初始化第一個結點
    this->frist = new DulNode;
    this->frist->prior = NULL;
    this->frist->next = NULL;
}

template<class T>
void DulLinkList<T>::AddElem(const T & data){
    // 直接頭部插入節點
    DulNode * newNode = new DulNode;
    newNode->data = data;
    newNode->next = this->frist;
    newNode->prior = NULL;
    this->frist->prior = newNode;
    this->frist = newNode;
}

template<class T>
void DulLinkList<T>::DelElem(const T & data){
    DulNode * current = this->frist->next;
    while (current  != NULL  && current->data != data) {
        current = current->next;
    }
    if (!current)
    {
        return;
    }
    Del(current);
}

template<class T>
void DulLinkList<T>::Del(DulNode * delNode){
    // 調整當前節點兩端的節點的指標
    delNode->prior->next = delNode->next;
    delNode->next->prior = delNode->prior;
    delete delNode;
}
template<class T>
DulLinkList<T>::~DulLinkList(){
    DulNode * current = this->frist;
    while (current)
    {
        DulNode * old = current;
        current = current->next;
        delete old;
    }
}

template<class T>
string DulLinkList<T>::ToString()const{
    string res;
    DulNode * current = this->frist->next;
    while (current)
    {
        res.append(1,current->data);
        current = current->next;
    }
    return res;
}

連結串列是個很基礎的東西,後面一些複雜的演算法或資料結構的本質也是一個連結串列。連結串列和順序表(也就是陣列)都可以再進一步抽象成更復雜的資料結構。

比如佇列和棧,不過是在連結串列或順序表的基礎上限制單端操作而已。再比如,由連結串列和順序表還可以構成二叉樹堆,它們還可以組合使用構成鄰接表,十字連結串列,鄰接多重表等結構用來描述圖,等等。

字串相關演算法

做裡快兩年web開發了,可以說字串是用多最多的資料型別了,所以針對字串的演算法也非常的多。先從簡單的慢慢來。

首先最基本的是對字串的求長,連線,比較,複製等

// 統計字串長度
int str_len(char *str){
    return *str ? str_len(str+1)+1 : 0 ;
}
// 字串複製
void str_cpy(char *str1,char *str2){
    while(*str1++ = *str2++); //當str2指向'\0'時,賦值給*str1 表示式的值為0 即為假。退出迴圈
        //if(*str1 == '\0')    // 考慮到 串2的長度大於串1的長度,防止指標越界
            //break;
}
// 字串比較
int str_cmp(char *str1,char *str2){
    int i;// i指向字元不同時陣列下標
    for(i=0;str1[i]==str2[i] && str1[i]!='\0' && str2[i]!='\0';i++);
    return str1[i]-str2[i];
}
// 字串連線
void str_cat(char *str1,char *str2){
    while(*str1 != '\0')
        str1++;
    while(*str1++ = *str2++);
}

字串比較複雜一點的就是模式匹配和子序列(編輯距離)的問題。

首先是較為簡單的BF演算法,這種演算法原理非常簡單,比如連個串a(主串)和b(模式串),首先將a1和b1進行比較,如果相同,則將b2與a2進行比較,如果還相同,繼續拿a3與b3比,直到b串匹配完,怎匹配完成,如果出現不同的,怎回到最初的狀態,將b1與a2進行比較,將b2與a3比較,等等,如此反覆直到失敗或成功。

typedef struct{
    char * str;
    int length;
}String;

int Index_BF(String mainStr,String modeStr,int pos){
    int    i = pos-1;
    int    j = 0;
    while (i<mainStr.length && j<modeStr.length)
    {
        if (mainStr.str[i] == modeStr.str[j])
        {
            i++,j++;
        }
        else{
            // 出現不同的,回退到模式第一個字元的狀態,將模式右移進行匹配
            i = i - j + 2;
            j = 0;
        }
    }
    if (j==modeStr.length)
    {
        return i - modeStr.length;
    }
    else{
        return 0;
    }
}

較為複雜的演算法是kmp演算法,KMP演算法的關鍵是避免BF演算法中的回朔。並且當匹配失敗後向右滑動到一個能自左最大對齊的位置繼續匹配。

若在ai,bj的位置匹配失敗,所以已經匹配的串便是

B1 B2 … Bj-1 == Ai-j+1 Ai-j+2 … Ai-1;

假設滑動完後要讓Bk與Ai對齊,則應該有

B1 B2 B3 … Bk-1 == Ai-k+1 A-k+2 … Ai-1;

因為是向右滑動,想一想i的位置不變,B向右滑動,很顯然,k要小於j。

所以進一步可以得到k到j之間B的子串(Bj前面的k-1個字元)與Ai前的k-1個字元是相同的,即

Bj-k+1 Bj-k+2 … Bj-1 == Ai-k+1 Ai-k+2 … Ai-1;

所以有

B1 B2 B3 … Bk-1  == Bj-k+1 Bj-k+2 … Bj-1

可以看出來,這個有個特點,字串 B1 B2 ….. Bj-1關於Bk有種對稱的感覺,不過這個不是映象對稱,不過我還是喜歡這麼記`對稱`,這也是求next值的依據,這個next就是k,就是偏移值。

next(j) = 0 (j==1) || max{k|1<=k<j && B1 B2 B3 … Bk-1  == Bj-k+1 Bj-k+2 … Bj-1} || 1;

下面是完整的KMP演算法

void GetNext(const char * mode,int * next){
    //求模式mode的next值並存入陣列next中
    int i=1,j=0;
    next[1] = 0;
    while(i < mode[0]){
        if(0 == j || mode[i] == mode[j]){
            i++;j++;
            next[i] = j;
        }
        else
            j = next[j];
    }
}
int Index_KMP(const char * str,const char * mode,int pos){
    int i=pos,j=1;
    int * next = (int *)malloc(sizeof(int)*(mode[0]+1));
    next[0]=mode[0];
    GetNext(str,next);
    while (i<=str[0] && j<= mode[0]) {
        if (0==j || str[i] == mode[j]) {
            i++;j++;
        }
        else{
            // 滑動模式串,注意next[j]是小於j的,這才是向右滑動
            j = next[j];
        }
    }
    return j>mode[0] ?  i - mode[0] : 0;
}

void main(){
    char string[16] = "\016abcabcabaabcac";
    char mode[10] = "\010abaabcac";
    printf("模式串在主串中的位置:%d\n",Index_KMP(string,mode,1));
}

下面的問題是最長公共子序列,演算法的思想是動態規劃,核心是轉義方程

,也就是當兩個字元相等時取左上元素+1,不相等時取左和上中大的那個

#include <stdio.h>
#include <string.h>
#define MAXN 128
#define MAXM MAXN
int a[MAXN][MAXM];
int b[MAXN][MAXM];
char * str1 = "ABCBDAB";
char * str2 = "BDCABA";

int LCS(const char *s1,int m, const char *s2,int n)
{
    int i, j;
    a[0][0] = 0;
    for( i=1; i <= m; ++i ) {
            a[i][0] = 0;
    }
    memset(b,0,sizeof(b));
    for( i=1; i <= n; ++i ) {
        a[0][i] = 0;
    }
    for( i=1; i <= m; ++i ){
        for( j=1; j <= n; ++j ){
            if(s1[i-1]==s2[j-1]){          //如果想等,則從對角線加1得來
                a[i][j] = a[i-1][j-1]+1;
                b[i][j] = 1;
            }
            else if(a[i-1][j]>a[i][j-1]){    //否則判段它上面、右邊的值,將大的數給他
                a[i][j]= a[i-1][j];
                b[i][j] = 2;
            }
            else{
                a[i][j] = a[i][j-1];
                b[i][j] = 3;
            }

        }
    }
    return a[m][n];
}
char str[MAXN];
int p=0;
void cSubQ(int i,int j){
    if(i<1 || j<1) return;
    if(1==b[i][j]){
        cSubQ(i-1,j-1);
        str[p++]=str1[i-1];
    }
    else if(2 == b[i][j])
    {
        cSubQ(i-1,j);
    }
    else{
        cSubQ(i,j-1);
    }
}
int main()
{
    int m = strlen(str1), n = strlen(str2);
    LCS(str1,m,str2,n);
    cSubQ(m,n);
    str[p] = '\0';
    printf("%s\n",str);
    return 0;
}

很顯然,這個演算法的時間複雜度和空間複雜度為o(n*m)

二叉樹

樹這裡主要以二叉樹為例,二叉樹算是一種特殊的樹,一種特殊的圖。

二叉樹具備如下特徵

  • 第i層最多有2^(i-1)次方個節點
  • 深度為k的樹最多有2^i-1個節點,也就是滿二叉樹等比求和
  • n0=n2+1,即葉子節點的數量恰好是度為2的節點數加1,主要原因是節點數總比度數多1,因為根節點沒有入度,所以有 n0 + n1 + n2 -1 = n1 + 2*n2。
  • 對於滿二叉樹,如果以有序表儲存,根節點放在0的位置上,左右孩子放在1,2上,相當於從上到下,從左到右,從0開始對節點進行編號,則對於節點i,它的左孩子應該位於2*i+1上,右孩子位於2*i+2上
  • 等等,暫時只記得這些了。

用陣列和連結串列都可以儲存二叉樹,但我見過的演算法大都用陣列儲存二叉樹,想必連結串列雖然易於理解,但相比寫起演算法來未必好寫。

對二叉樹的操作

有增刪查遍歷等操作,程式碼如下。

typedef struct bitnode{
    int m_iDate;
    struct bitnode * m_lChild/*左孩子指標*/,* m_rChild/*右孩子指標*/;        
} CBiTNode;

//建立一個帶頭結點的空的二叉樹
CBiTNode * Initiate(){
    CBiTNode * bt;
    bt = (CBiTNode*)malloc(sizeof CBiTNode);
    bt->m_iDate = 0;
    bt->m_lChild = NULL;
    bt->m_rChild = NULL;
    return bt;
}

/*
//建立一個不帶頭結點的空的二叉樹
CBiTNode * Initiate(){
    CBiTNode * bt;
    bt = NULL;
    return bt;
}
*/

//生成一棵以x為根節點資料域資訊,以lbt和rbt為左右子樹的二叉樹
CBiTNode * Create(int x,CBiTNode * lbt,CBiTNode * rbt){
    CBiTNode * p;
    if((p=(CBiTNode*)malloc(sizeof CBiTNode)) ==NULL)
        return NULL;
    p->m_iDate = x;
    p->m_lChild = lbt;
    p->m_rChild = rbt;
    return p;
}

//在二叉樹bt中的parent所指節點和其左子樹之間插入資料元素為x的節點
bool InsertL(int x,CBiTNode * parent){
    CBiTNode * p;

    if(NULL == parent){
        printf("L插入有誤");
        return 0;
    }

    if((p=(CBiTNode*)malloc(sizeof CBiTNode)) ==NULL)
        return 0;

    p->m_iDate = x;
    p->m_lChild = NULL;
    p->m_rChild = NULL;

    if(NULL == parent->m_lChild)
        parent->m_lChild = p;
    else{
        p->m_lChild = parent->m_lChild;
        parent->m_lChild = p;
    }

    return 1;
}

//在二叉樹bt中的parent所指節點和其右子樹之間插入資料元素為x的節點
bool InsertR(int x,CBiTNode * parent){
    CBiTNode * p;

    if(NULL == parent){
        printf("R插入有誤");
        return 0;
    }

    if((p=(CBiTNode*)malloc(sizeof CBiTNode)) ==NULL)
        return 0;

    p->m_iDate = x;
    p->m_lChild = NULL;
    p->m_rChild = NULL;

    if(NULL == parent->m_rChild)
        parent->m_rChild = p;
    else{
        p->m_rChild = parent->m_rChild;
        parent->m_rChild = p;
    }

    return 1;
}

//在二叉樹bt中刪除parent的左子樹
bool DeleteL(CBiTNode *parent){
    CBiTNode * p;
    if(NULL == parent){
        printf("L刪除出錯");
        return 0;
    }

    p = parent->m_lChild;
    parent->m_lChild = NULL;
    free(p);//當*p為分支節點時,這樣刪除只是刪除了子樹的根節點。子樹的子孫並沒有被刪除

    return 1;
}

//在二叉樹bt中刪除parent的右子樹
bool DeleteR(CBiTNode *parent){
    CBiTNode * p;
    if(NULL == parent){
        printf("R刪除出錯");
        return 0;
    }

    p = parent->m_rChild;
    parent->m_rChild = NULL;
    free(p);//當*p為分支節點時,這樣刪除只是刪除了子樹的根節點。子樹的子孫並沒有被刪除

    return 1;
}

//二叉樹的遍歷
//先序遍歷二叉樹
bool PreOrder(CBiTNode * bt){
    if(NULL == bt)
        return 0;
    else{
        printf("bt->m_iDate == %d\n",bt->m_iDate);
        PreOrder(bt->m_lChild);
        PreOrder(bt->m_rChild);
        return 1;
    }
}

對二叉樹可以有先序遍歷,中序遍歷,後序遍歷得到的序列中每個元素互第一個和最後一個節點外都會有一個前驅和後驅節點。如果把前驅節點和後驅節點的資訊儲存在節點中就構成了線索二叉樹,顯然只要免禮一遍就能得到線索二叉樹。

二叉樹比較經典的有哈夫曼編碼問題,二叉堆等問題,二叉堆放到堆排序一起講。

哈夫曼問題就是要讓頻率高的節點編碼最短,也就是要節點在哈夫曼樹中的深度最小。

//    Huffman.h
#ifndef __HUFFMAN_H__
#define __HUFFMAN_H__

typedef struct {
    unsigned int weight;
    unsigned int parent,lchild,rchild;
}HTNode,* HuffmanTree;            // 動態分配陣列儲存哈夫曼樹
typedef char * *HuffmanCode;    // 動態分配陣列儲存哈夫曼編碼表

#endif//__HUFFMAN_H__
//    HuffmanTest.cpp
#include "Huffman.h"
#include <string.h>
#include <malloc.h>
// 函式功能:在哈夫曼編碼表HT[1...range]中選擇 parent 為0且weight最小的兩個結點,將序號分別存到s1和s2裡
void Select(const HuffmanTree &HT,int range,int &s1,int &s2){
    int i;
    int * pFlags;
    pFlags = (int *)malloc((range+1)*sizeof(int));
    int iFlags=0;
    for(i=1;i<=range;i++){
        if(0 == HT[i].parent){
            pFlags[++iFlags] = i;
        }
    }
    int Min=1000;
    int pMin=pFlags[1];
    for(i=1;i<=iFlags;i++){
        if(HT[pFlags[i]].weight < Min){
            pMin = i;
            Min=HT[pFlags[i]].weight;
        }
    }
    s1=pFlags[pMin];
    Min=1000;
    for(i=1;i<=iFlags;i++){
        if(pFlags[i]!=s1)
            if(HT[pFlags[i]].weight < Min){
                pMin = i;
                Min=HT[pFlags[i]].weight;
            }
    }
    s2=pFlags[pMin];
}
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int * w, int n){
    // w存放n個字元的權值(均>0),構造哈夫曼樹HT,並求出n個字元的哈夫曼編碼HC
    if(n <= 1) return;
    int m = 2*n-1;
    HT = (HuffmanTree)malloc((m+1)*sizeof(HTNode));        // 0 單元不使用
    int i;
    HuffmanTree p=NULL;
    // 初始化哈夫曼編碼表
    for(p=HT+1,i=1;i<=n;i++,p++,w++){
        p->weight = *w,p->parent = p->lchild = p->rchild = 0;
    }
    for( ;i<=m;i++,p++){
        p->weight = p->parent = p->lchild = p->rchild = 0;
    }
    // 建立哈夫曼樹
    int s1,s2;
    for(i=n+1;i<=m;i++){
        Select(HT,i-1,s1,s2);
        HT[s1].parent = HT[s2].parent = i;
        HT[i].lchild = s1,HT[i].rchild = s2;
        HT[i].weight = HT[s1].weight+HT[s2].weight;
    }

    // 從葉子節點到根逆向求每個字元的哈夫曼編碼
    HC = (HuffmanCode)malloc((n+1)*sizeof(char *));        // 分配n個字元編碼的頭指標向量
    char * cd = (char*)malloc(n*sizeof(char));            // 分配求編碼的工作空間
    int start;                                            // 編碼起始位置
    cd[n-1] = '\0';                                        // 編碼結束符
    for(i=1;i<=n;i++){                                    // 逐個字元求哈夫曼編碼
        start = n-1;                                    // 將編碼起始位置和末位重合
        for(int c=i,f=HT[i].parent;f != 0; c=f,f=HT[f].parent){
            if(c == HT[f].lchild)
                cd[--start] = '0';
            else
                cd[--start] = '1';
        }
        HC[i] = (char*)malloc((n-start)*sizeof(char));    // 為第i個字元編碼分配空間
        strcpy(HC[i],&cd[start]);                        // 從 cd 複製字串到 HC
    }
    free(cd);
}

哈夫曼樹的測試資料

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

void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int * w, int n);
void main(){
    system("color F0");
    HuffmanTree HT = NULL;
    HuffmanCode HC = NULL;
    char chArr[8]={'A','B','C','D','E','F','G','H'};
    int w[8]={7,19,2,6,32,3,21,10};
    HuffmanCoding(HT,HC,w,8);
    int i,j;
    printf(    "HT    weight   parent  lchild  rchild\n");
    for(i=1;i<=15;i++){
        printf("%02d\t%2u\t%2u\t%2u\t%2u\n",i,HT[i].weight,HT[i].parent,HT[i].lchild,HT[i].rchild);
    }
    printf("\n\n字元    權值    編碼\n");
    for(i=0;i<8;i++){
        printf("%c\t%2d\t%-8s\n",chArr[i],w[i],HC[i+1]);
    }
}

圖相關演算法

圖是一種比較複雜的資料結構,圖的定義就不在此複述了。

圖的一些表示方法(儲存結構)

  • 鄰接矩陣
    • 對於一個又n個節點的圖,鄰接矩陣以一個n*n的二維陣列a來描述圖,對於不同的圖,比如,有向圖和無向圖,帶權圖和無權圖,a[i,j]表示的含義有所不同,但都是描述邊的。
  • 鄰接表
    • 鄰接表組合使用陣列和連結串列描述圖,其中陣列的每一個元素代表一個節點i,i由兩部分組成,一部分代表節點的資料,另一部分為一個指向一連結串列,這個連結串列裡存放著能從節點i出發能走到的所有節點。對於有向圖和無向圖會有不同的表示。鄰接表一般要比領接矩陣更省空間,但它帶來了求入度不便等問題。
  • 十字連結串列
    • 結合使用鄰接表與逆鄰接表,這種方式只能描述有向圖。首先它也有一個陣列,每個資料元素代表一個節點i,i由三部分組成,i在鄰接表的基礎上增加了一個指標,這個指標指向第一個以i為弧尾的及節點。這就很好的解決了求入度的問題。
  • 鄰接多重表
    • 鄰接多重表主要,它主要用來表描述無向圖,在鄰接表或十字連結串列中,陣列元素的指標域指向的連結串列元素其實代表了邊,如果用鄰接表來存無向圖,會使得一條邊對應的兩個節點分別位於兩條鏈中,當我需要刪除一條邊時,總是需要找到另一個表示這條邊的邊節點,再刪除。所以有了鄰接多重表,鄰接多重表就是隻用一個邊界點表示邊,但是將它連結到兩連結串列中(對,沒有錯,一個節點,同時存在於兩個連結串列中)

下面是上面四種描述的程式碼表示

#ifndef __GRAPH_DEFINE_H__
#define __GRAPH_DEFINE_H__

#define INT_MAX            9999
#define INFINITY        INT_MAX            // 最大值
#define MAX_VERTEX_NUM    20                // 最大頂點個數
#define VEX_FORM        "%c"            // 頂點輸入格式
#define INFO_FORM        "%d"            // 邊資訊輸入格式
typedef int InfoType;            // 弧相關資訊型別
typedef char VextexType;        // 頂點資料型別
typedef int VRType;                // 頂點關係型別,對無權圖,用0或者1表示是否相鄰。對帶權圖,則是權值型別。
typedef enum { DG,DN,UDG,UDN} GraphKind;// 圖型別 {有向圖,有向網,無向圖,無向網}

//////////////////////////////////////////////////////////////////////////
//    鄰接矩陣儲存結構: 可儲存任意型別圖
//////////////////////////////////////////////////////////////////////////
typedef struct{
    VRType        Adj;
    InfoType    Info;                    // 該弧相關資訊
}ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef struct{
    char cVexs[MAX_VERTEX_NUM];            // 頂點向量
    AdjMatrix arcs;                        // 鄰接矩陣
    int iVexNum,iArcNum;                // 圖中當前頂點數和弧數
    GraphKind kind;                        // 圖的種類標誌
}MGraph;

//////////////////////////////////////////////////////////////////////////
//    鄰接表儲存結構:    可儲存任意型別的圖
//////////////////////////////////////////////////////////////////////////
typedef struct ArcNode{
    int                iAdjvex;        // 該弧所指向的頂點位置
    struct ArcNode    *nextarc;        // 指向下一條弧的指標
    InfoType        Info;            // 該弧相關資訊
}ArcNode;
typedef struct VNode{
    VextexType    cData;                // 頂點資訊
    ArcNode        *firstarc;            // 指向第一條依附該頂點的弧的指標
}VNode,AdjList[MAX_VERTEX_NUM];
typedef struct {
    AdjList        vertices;
    int            iVexnum,iArcnum;    // 圖的當前頂點個數和弧數
    GraphKind    Kind;                // 圖的種類標誌
}ALGraph;

//////////////////////////////////////////////////////////////////////////
//    十字連結串列儲存結構: 只儲存有向圖
//////////////////////////////////////////////////////////////////////////
typedef struct ArcBox{
    int                iTailVex,iHeadVex;        // 該弧的尾和頭頂點的位置
    struct ArcBox    *hLink,*tLink;            // 分別為弧頭相同和弧尾相同的鏈域
    InfoType        Info;                    // 該弧相關資訊
}ArcBox;
typedef struct VexNode{
    VextexType        data;
    ArcBox            *firstIn,*firstOut;        // 分別指向了該頂點的第一條入弧和出弧
}VexNode;
typedef struct OLGraph{
    VexNode        xlist[MAX_VERTEX_NUM];        // 表頭向量
    int            iVexNum,iArcNum;            // 有向圖當前頂點數和弧數
}OLGraph;

//////////////////////////////////////////////////////////////////////////
//    鄰接多重表:    儲存無向圖
//////////////////////////////////////////////////////////////////////////
typedef enum {unvisited,visited}VisitIf;
typedef struct EBox{
    VisitIf            mark;            // 訪問標記
    int                iIVex,iJVex;    // 邊依附的兩個頂點的位置
    struct EBox        *iLink,*jLink;    // 分別指向依附這兩個頂點的下一條邊
    InfoType        Info;            // 該邊資訊指標
}EBox;
typedef struct VexBox{
    VextexType        data;
    EBox            *firstEdge;        // 指向第一條依附該頂點的邊
}VexBox;
typedef struct {
    VexBox            adjmulist[MAX_VERTEX_NUM];
    int                iVexNum,iEdgeNum;    // 無向圖當前頂點數和邊數
}
#endif//__GRAPH_DEFINE_H__

圖相關操作

對圖的操作有建立,增刪查等等,其中查就是遍歷,遍歷分為深度優先搜尋和廣度優先搜尋。

深度優先搜尋類似於樹的鐘旭遍歷,即遇到未範圍的節點,馬上訪問,修改標記陣列,然後沿著這個節點繼續訪問,直到訪問完,然後在回朔,轉向未訪問的分支。直到節點被訪問完,如果是連通圖,只要訪問進行一次深度搜尋,如果是非連通的,就要搜尋多次。

廣度優先搜尋就像金字塔從上向下的一層一層的搜尋,廣度優先搜尋除了需要用標記陣列記錄狀態以外,還需要用佇列來將發現而未訪問的節點記錄下來。用佇列是為了保證遍歷順序。

下面是一些圖相關的操作演算法

#include <stdio.h>
#include "GraphDefine.h"
#include "Define.h"
#include <malloc.h>

//////////////////////////////////////////////////////////////////////////
//    鄰接矩陣圖的相關操作
//////////////////////////////////////////////////////////////////////////
Status CreateDG(MGraph &G);        // 構造有向圖
Status CreateDN(MGraph &G);        // 構造有向網
Status CreateUDG(MGraph &G);    // 構造無向圖
Status CreateUDN(MGraph &G);    // 構造無向網
int LocateVex(const MGraph &G,const VextexType &v);    // 獲得頂點v在圖中的位置
Status CreateGraph(MGraph &G,VextexType v){
    // 採用陣列(鄰接矩陣)表示法,構造圖G
    int iType;
    scanf("%d%*c",&iType);
    G.kind = (GraphKind)iType;
    switch(G.kind) {
    case DG: return CreateDG(G);        // 構造有向圖
    case DN: return CreateDN(G);        // 構造有向網
    case UDG:return CreateUDG(G);        // 構造無向圖
    case UDN:return CreateUDN(G);        // 構造無向網
    default:
        return ERROR;
    }
}

Status CreateUDN(MGraph &G){
    //  採用陣列(鄰接矩陣)表示法,構造網G
    int i,j,k;
    int IncInfo;
    scanf("%d %d %d",&G.iVexNum,&G.iArcNum,&IncInfo);
    for(i=0;i<G.iVexNum;++i)            // 構造頂點向量
        scanf("%c%*c",&G.cVexs[i]);
    for(i=0;i<G.iVexNum;i++){
        for(j=0;j<G.iVexNum;j++){        // 初始化鄰接矩陣
            G.arcs[i][j].Adj = INFINITY;
            G.arcs[i][j].Info =NULL;        
        }
    }
    VextexType v1,v2;
    int w;
    for(k=0;k<G.iArcNum;k++){            // 構造鄰接矩陣
        scanf("%c %c %d%*c",&v1,&v2,&w);
        i = LocateVex(G,v1),j = LocateVex(G,v2);    // 確定v1,v2在G中的位置
        G.arcs[i][j].Adj = w;            // 弧<v1,v2>的權值
        if(IncInfo) scanf(INFO_FORM,G.arcs[i][j].Info);
        G.arcs[j][i] = G.arcs[i][j];    // 置<v1,v2>對稱弧<v2,v1>
    }
    return OK;
}

//////////////////////////////////////////////////////////////////////////
//    十字連結串列圖的相關操作
//////////////////////////////////////////////////////////////////////////
int LocateVex(const OLGraph &G,const VextexType &v);    // 獲得頂點v在圖中的位置

Status CreateDG(OLGraph &G){
    // 採用十字連結串列儲存表示,構造有向圖 G(G.kind = DG)
    InfoType IncInfo;
    scanf("%d %d %d",G.iVexNum,G.iArcNum,&IncInfo);
    int i;
    for(i=0;i<G.iVexNum;i++){            // 構造表頭向量
        scanf(VEX_FORM "%*c",G.xlist[i].data);            // 輸入頂點值
        G.xlist[i].firstIn = G.xlist[i].firstOut = NULL;// 初始化指標
    }
    int k,j;
    VextexType    v1,v2;
    ArcBox * p;
    for(k=0;k<G.iArcNum;k++){            // 輸入各弧並構造十字連結串列 
        scanf(VEX_FORM VEX_FORM,&v1,&v2);            // 輸入一條弧的始點和終點
        i = LocateVex(G,v1),j = LocateVex(G,v2);    // 確定V1和 V2在G中的位置
        p = (ArcBox*)malloc(sizeof(ArcBox));        // 假設有足夠的空間
        // 對弧節點賦值
        p->iTailVex = v1,p->iHeadVex = v2;
        p->hLink = G.xlist[j].firstIn,p->tLink = G.xlist[i].firstOut;
        p->Info = NULL;
        G.xlist[j].firstIn = G.xlist[i].firstOut = p;    // 完成在入弧與出弧的鏈頭的插入
        if(IncInfo) scanf(INFO_FORM,&p->Info);            //若含有相關資訊,則輸入
    }
    return OK;
}

//////////////////////////////////////////////////////////////////////////
//    深度優先搜尋
//////////////////////////////////////////////////////////////////////////
bool visited[MAX_VERTEX_NUM];
Status(*VisitFunc)(int v);
int FirstAdjVex(MGraph,int);
int NextAdjVex(MGraph,int);

void  DFSTeaverse(MGraph G,Status (*Visit)(int v)){
    int v;
    VisitFunc = Visit;
    for(v=0;v<G.iVexNum;v++) visited[v] = false;
    for(v=0;v<G.iVexNum;v++)
        if(!visited[v]) DFS(G,v);
}

void DFS(MGraph G,int v){
    visited[v] = true;
    VisitFunc(v);
    int w;
    for(w = FirstAdjVex(G,v); w>0 ; w = NextAdjVex(G,v))
        if(!visited[w]) DFS(G,w);
}
//////////////////////////////////////////////////////////////////////////
//    廣度優先搜尋
//////////////////////////////////////////////////////////////////////////
//    佇列相關函式
void InitQueue(int []);
void EnQueue(int []);
bool QueueEmpty(int []);
void DFSTeaverse(MGraph G,Status (*Visit)(int v)){
    int v;
    for (v=0;v<G.iVexNum;v++)
    {
        visited[v] = false;
    }
    int Q[MAX_VERTEX_NUM];
    InitQueue(Q);
    for(v=0;v<G.iVexNum;v++){
        if(!visited[v]){
            visited[v] = true;
            Visit(v);
            EnQueue(Q);
            while(!QueueEmpty(Q)){
                int u,w;
                DeQueue(Q,u);
                for( w = FirstAdjVex(G,u); w>=0 ; w = NextAdjVex(G,u))
                    if(!visited[w]){
                        visited[w] = true;
                        Visit(w);
                        EnQueue(Q,w);
                    }
            }
        }
    }
}

與圖相關的還有很多演算法,比如求最小生成樹的prim演算法和kruskal演算法

prim演算法初始化一個s集合,始終挑選與s集合相連最小的邊連線的節點加到集合中,然後更新剩餘節點到s的距離,直到所有的點新增進了s集合,prim演算法程式碼如下

#include <stdio.h>
#include <string.h>
#define MAXN 1024
#define INF 0xeFFFFFFF
int e[MAXN][MAXN];
int low[MAXN];
bool inSet[MAXN];
int n;
int prim(){
    int res=0,i,j,k;
    inSet[0] = true;
    memset(inSet,0,sizeof(inSet));
    for(i=0;i<n;i++){
        //現在所有點到S的距離就是到0的距離
        low[i] = e[0][i];
    }
    for(i=1;i<n;i++){
        int minv = INF;
        int p = 0;
        //找出與集合s相連的最小的邊
        for(j=0;j<n;j++){
            if(!inSet[j] && low[j] < minv){
                minv = low[j],p = j;
            }
        }
        if(minv == INF) return -1;//非連通圖
        //將頂點j加到S裡面
        inSet[p] = true;
        //將最短路徑加到結果裡
        res += low[p];
        //更新low陣列
        for(k=0;k<n;k++){
            if(!inSet && low[p] > e[p][k]){
                low[p] = e[p][k];
            }
        }
    }

    return res;
}

int main(void){
    int i;
    scanf("%d",&n);
    for(i=0;i<n-1;i++){
        int x,y,w;
        scanf("%d%d%d",&x,&y,&w);
        e[x][y] = e[y][x] = w;
    }
    int res = prim();
    printf("%d\n",res);
    return 0;
}

Kruskal演算法不斷選取最小的邊i,只要biani加進來不構成迴路,則加入到邊的集合e中來,直到加入的邊能連線所有的頂點,則結束演算法

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAXN 1024
#define INF 0xeFFFFFFF

//定義邊的結構
typedef struct Ele{
    int x,y;    //邊的端點
    int w;      //邊的權重
    bool inSet;
}Ele;

Ele * eles[MAXN];//對於n個頂點,最多有n*(n-1)條邊

int m;//m條邊

int pa[MAXN];
int r[MAXN];

void make_set(){
    int i;
    for(i=0;i<n;i++){
        pa[i] = i;
        r[i] = 1;
    }
}

int find_set(int x){
    if(x != pa[x]){
        pa[x] = find_set(x);
    }
    return pa[x];
}

bool unin_set(int x,int y){
    if(x==y) return;
    int xp = find_set(x);
    int yp = find_set(y);
    if(xp == yp) return false;//構成迴路,不能合併
    if(r[xp]>r[yp]){
        pa[yp] = xp;//zhi小的放在zhi大的下面
    }
    else{
        pa[xp] = yp;
        if(r[xp] == r[yp]){
            r[yp]++;
        }
    }
    return true;
}

void sort(){
    int i,j;
    for(i=0;i<n-2;i++){
        int p = i;
        for(j = i+1;j<n-1;j++){
            if(eles[p].w > eles[j].w){
                p = j;
            }
        }
        if(p!=i){
            Ele * tmp = eles[i];eles[i] = eles[p];eles[p] = tmp;
        }
    }
}
/*
int cmp(void * a,void * b){
    return (Ele*)a->w - (Ele*)b->w;
}
*/
int klske(){
    //將邊由小到大排序
    //qsort(eles,sizeof(eles),sizeof(eles[0]),cmp)
    sort();
    int res;
    for(int i=0;i<m;i++){
        if(unin_set(find_set(eles[i].x),find_set(eles[i].y))){
            printf("%d %d %d\n",else[i].x,eles[i].y,eles[i].w);
        }
    }
    return res;
}

int main(void){
    int i;
    scanf("%d",&m);
    //eles = (Ele*)malloc(n*sizeof(Ele));
    for(i=0;i<m-1;i++){
        int x,y,w;
        scanf("%d%d%d",&x,&y,&w);
        eles[i] = (Ele*)malloc(sizeof(Ele));
        eles[i]->x = x;
        eles[i]->y = y;
        eles[i]->w = w;
        eles[i]->inSet = false;
    }
    int res = klske(k);
    printf("%d\n",res);

    for(i=0;i<m-1;i++){
        free(eles[i]);
    }
    return 0;
}

上面主要涉及的是一些資料結構,以及這些資料結構最基本的演算法,下面進入演算法部分

查詢演算法

樹表查詢

線索二叉樹

線索二叉樹要求任何幾節點的左子樹比該節點的值小,右子樹的值比該節點大。二叉排序樹,主要涉及的是插入和搜尋

#include <stdio.h>
#include <malloc.h>

typedef struct bsTree{
    int m_iDate;
    struct bsTree * m_lChild/*左孩子指標*/,* m_rChild/*右孩子指標*/;
} * BsTree,BsNode ;

BsTree  insert(BsTree  bs,int x){
    BsNode * p = bs;
    BsNode * note  = p;
    BsNode * ct = NULL;
    while(p){
        if(x == p->m_iDate){
            return p;
        }
        else{
            // 記錄上一個節點
            note = p;
            if(x < p->m_iDate) p = p->m_lChild;
            else p = p->m_rChild;
        }
    }

    ct = (BsNode * )malloc(sizeof(BsNode));
    ct->m_iDate = x;
    ct->m_rChild = NULL;
    ct->m_lChild = NULL;
    if(!bs){
        bs = ct;
    }else if(x < note->m_iDate){
        note->m_lChild = ct;
    }else note->m_rChild = ct;
    return bs;
}

BsNode * search(BsTree bs , int x){
    if(!bs || bs->m_iDate == x){
        return bs;
    }else if(x < bs->m_iDate){
        return search(bs->m_lChild,x);
    }else{
        return search(bs->m_rChild,x);
    }
}

有序表查詢

二分查詢

int binarySearch(int arr[],int l,int r,int key){
    while(l<r){
        int mid = (l+r) >> 1;
        if(arr[mid] > key){
            r = mid -1;
        }else if(arr[mid] < key){
            l = mid + 1;
        }else{
            return mid;
        }
    }
    return -l-1; // 返回可插入位置
}

排序演算法

氣泡排序

以升序為例,氣泡排序每次掃描陣列,將逆序修正,每次恰好將最大的元素過五關斬六將推到最後,再在剩餘的元素重複操作

void mpSort(int a[],int n){
    int i,j,swaped=1;
    for(i=0;i<n-1 && swaped;i++){
        swaped = 0;
        for(j=0;j<n-i;j++){
            if(a[j] > a[j+1]){
                swap(a[j],a[j+1]);
                swaped = 1;
            }
        }
    }
}

可見,氣泡排序的平均時間複雜度為O(n^2)

選擇排序

以升序為例,每次掃描陣列,找到最小的元素直接挪到第一個來,再在剩餘的陣列中重複這樣的操作

void selectSort(int a[],int n){
    int i,j;
    for(i=0;i<n-1;i++){
        int p =i;
        for(j=i;j<n;j++){
            if(a[p] > a[j]){
                p = j;
            }
        }
        if(p != i){
            swap(p,i);
        }
    }
}

選擇排序的平均時間複雜度也是O(n^2)

直接插入排序

直接插入排序不斷在前面已經有序的序列中插入元素,並將元素向後挪。再重取一個元素重複這個操作

#define MAXSIZE        20

typedef int KeyType;
typedef struct {
    KeyType key;
}RedType;
typedef struct{
    RedType r[MAXSIZE+1];
    int length;
}SqList;
//---------------------直接插入排序---------------------
void InsertSort(SqList & L){
    int i,j;
    for (i=2;i<=L.length;i++)
    {
        L.r[0] = L.r[i];
        j = i -1;
        while (L.r[0].key < L.r[j].key)
        {
            L.r[j+1] = L.r[j];
            j--;
        }
        L.r[j+1] = L.r[0];
    }
}

插入排序的平均時間複雜度也是O(n^2)

堆排序

堆排序也是一種插入排序,不過是向二叉堆中插入元素,而且以堆排序中的方式儲存二叉堆,則二叉堆必定是一棵完全二叉樹,堆排序設計的主要操作就是插入和刪除之後的堆調整,使得堆保持為大底堆或者小底堆的狀態。也就是根節點始終保持為最小或最大,每次刪除元素,就將根節點元素與最後元素交換,然後將堆的大小減1,然後進行堆調整,如此反覆執行這樣的刪除操作。很顯然,大底堆最後會得到遞增序列,小底堆會得到遞減序列。

/**
*  堆調整
*   在節點數為n的堆中從i節點開始進行堆調整
*/
void heapAjust(int arr[] ,int i,int n){
    // 從i節點開始調整堆成為小底堆
    int tmp = arr[i],min;
    // 左孩子,對於節點i,它的左孩子就是2i+1,右孩子就是2i+2;
    for(;(min = 2*i + 1)<n;i = min){
        if(min+1 != n && arr[min] > arr[min+1]) min ++;
        if(arr[i] > arr[min]){
            //  將小元素放置到堆頂
            arr[i] = arr[min];
        }
        else break;
    }
    arr[i] = tmp;
}
void heapSort(int arr[],int n){
    // 建堆
    int i;
    for(i=n>>1;i>=0;i--){
        heapAjust(arr,i,n);
    }
    // 取堆頂,調整堆
    for(i = n-1;i>0;i--){
        //  每次取堆頂最小的元素與最後的元素交換,最後會得到遞減序列
        int tmp = arr[0];arr[0] = arr[i],arr[i] = tmp;
        //  刪除一個元素後需要從根元素起重新調整堆
        heapAjust(arr,0,i);
    }
}

排序的平均時間複雜度為O(NLogN)

快速排序

說到快排,就想起了大一上學期肖老師(教我C語言的老師)與我討論的問題,當時懵懂無知,後才才知道那就是快排。

快排的思想也很簡單,以升序為例,在序列中選一標杆,一般講第一個元素作為標杆,然後將序列中比標杆小的元素放到標杆左邊,將比標杆大的放到標杆右邊。然後分別在左右兩邊重複這樣的操作。

void ksort(int a[],int l,int r){
    //  長度小於2有序
    if(r - l < 2) return;
    int start = l,end = r;
    while(l < r){
        // 向右去找到第一個比標杆大的數
        while(++l < end && a[l] <= a[start]);
        // 向左去找第一個比標杆小的數
        while(--r > start && a[r] >= a[start]);
        if(l < r) swap(a[l],a[r]); // 前面找到的兩個數相對於標杆逆序 ,需交換過來 。l==r 不需要交換,
    }
    swap(a[start],a[r]);// 將標杆挪到正確的位置.
    // 對標杆左右兩邊重複演算法,注意,這個l已經跑到r後面去了
    ksort(a,start,r);
    ksort(a,l,end);
}

快速排序的平均時間複雜度為O(NLogN)

合併排序

合併排序採用分治法的思想對陣列進行分治,對半分開,分別對左右兩邊進行排序,然後將排序後的結果進行合併。按照這樣的思想,遞迴做是最方便的。

int a[N],c[N];
void mergeSort(l,r){
    int mid,i,j,tmp;
    if(r - 1 > l){
        mid = (l+r) >> 1;
        // 分別最左右兩天排序
        mergeSort(l,mid);
        mergeSort(mid,r);
        // 合併排序後的陣列
        tmp  = l;
        for(i=l,j=mid;i<mid && j<r;){
            if(a[i] > a[j]) c[tmp++] = a[j++];
            else c[tmp++] = a[i++];
        }
        //  把剩餘的接上
        if(j < r) {
            for(;j<r;j++) c[tmp++] = a[j];
        }else{
            for(;i<mid;i++) c[tmp++] = a[i];
        }
        // 將c陣列覆蓋到a裡
        for(i=l;i<r;i++){
            a[i] = c[i];
        }
    }
}

先到這裡吧,寫這麼多有點辛苦,白天還有事。

相關文章