数据结构与算法基础整理

目录

1. 数据结构(8种数据结构)
2. 排序(7种排序)
3. 查找(7种查找)
4. 树
5. 图(BFS、DFS、最小生成树、最短路径)

一. 数据结构

1.顺序表和链表

顺序表使用数组实现,采用一组地址连续的存储单元,数组大小有两种方式指定,一是静态分配,二是动态扩展。
链表的定义是递归的,它或者为null,或指向另一个节点node的引用,这个节点含有下一个节点或者链表的引用。与顺序存储相比,允许存储空间不连续,插入、删除元素不需要移动大量元素,只需要修改指针即可,但查找某个元素,只能从头遍历整个链表。
两者区别在于:

  1. 当线性表需要频繁查找,较少插入和删除时,宜采用顺序存储结构。若需要频繁插入和删除,宜采用单链表。
  2. 当线性表的元素个数变化较大或不确定时,最好用单链表,这样不需要考虑存储空间大小问题。当事先知道线性表的大小长度,用顺序存储结构效率会高一些。

链表分为单链表、双链表、循环链表和静态链表等。双向链表存在两个缺点:一是指针数的增加会导致存储空间需求增加;二是添加和删除数据时需要改变更多指针的指向。在链表尾部使用指针,并且让它指向链表头部的数据,将链表变成环形。这便是“循环链表”。

2.数组

数组也是数据呈线性排列的一种数据结构,数据按顺序存储在内存的连续空间内。在链表和数组中,数据都是线性地排成一列。在链表中访问数据较为复杂,添加和删除数据较为简单;而在数组中访问数据比较简单,添加和删除数据却比较复杂。

3.栈

栈是限定仅在表尾进行插入和删除操作的线性表(后进先出)。添加和删除数据的操作只能在一端进行,访问数据也只能访问到顶端的数据。想要访问中间的数据时,就必须通过出栈操作将目标数据移到栈顶才行。允许插入和删除的一端称为栈顶,另一端称为栈底。

  1. 栈的顺序存储结构其实也是线性表的顺序存储,简称顺序栈。
  2. 栈的链式存储结构简称链栈。栈顶在单链表头部,对链栈来说不需要头结点。

4.队列

队列是只允许在一端就行插入操作、在另一端进行删除操作的线性表(先进先出)。在队列中添加和删除则分别是在两端进行,队列也不能直接访问位于中间的数据,必须通过出队操作将目标数据变成首位后才能访问。允许插入的一端称为队尾,允许删除的一端称为队头。
队列作为一种特殊的线性表,也有顺序存储和链式存储。

5.哈希表

哈希表存储的是由键(key)和值(value)组成的数据,存储位置重复了的情况便叫作“冲突”。遇到这种情况,可使用链表在已有数据的后面继续存储新的数据。在哈希表中,我们可以利用哈希函数快速访问到数组中的目标数据。如果发生哈希冲突,就使用链表进行存储。这样一来,不管数据量为多少,我们都能够灵活应对。
在存储数据的过程中,如果发生冲突,可以利用链表在已有数据的后面插入新数据来解决冲突。这种方法被称为“链地址法”。

6.堆(小根堆、大根堆)

堆是一种图的树形结构,在堆的树形结构中,各个顶点被称为“结点”,数据就存储在这些结点中。对于小根堆: 1)堆中的每个结点最多有两个子结点,结点的排列顺序为从上到下,同一行里则为从左到右。2)子结点必定大于父结点,从堆中取出数据时,取出的是最上面的数据。这样,堆中就能始终保持最上面的数据最小(最上面的数据被取出、将最后的数据移动到最顶端、如果子结点的数字小于父结点的,就将父结点与其左右两个子结点中较小的一个进行交换、重复这个操作直到数据都符合规则)。
假设数据量为n,根据堆的形状特点可知树的高度为 log2n ,那么重构(添加)树的时间复杂度便为O(logn)。

7.二叉查找树

二叉查找树(又叫作二叉搜索树或二叉排序树)是一种数据结构,采用了图的树形结构,数据存储于二叉查找树的各个结点中。二叉查找树有两个性质。第一个是每个结点的值均大于其左子树上任意一个结点的值;第二个是每个结点的值均小于其右子树上任意一个结点的值。得到结论1)二叉查找树的最小结点要从顶端开始,往其左下的末端寻找。2)二叉查找树的最大结点要从顶端开始,往其右下的末端寻找。
如果结点数为 n,而且树的形状又较为均衡的话,比较大小和移动的次数最多就是 log2n。因此,时间复杂度为 O(logn)。但是,如果树的形状朝单侧纵向延伸,树就会变得很高,此时时间复杂度也就变成了 O(n)。

8. 图

图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为G(V,E),G表示一个图,V(vertex)是顶点的集合,E(edge)是边的集合。图中数据元素称为顶点,任意两顶点之间的逻辑关系用变表示。图的存储结构包含邻接矩阵、邻接表、边集数组、十字链表、邻接多重表等。图的遍历分为深度优先遍历和广度优先遍历两种。

线性结构: 链表、 数组、栈、队列、哈希表
图形结构: 堆、二叉查找树、图


二. 排序

1.冒泡排序

冒泡排序就是重复“从序列右边开始比较相邻两个数字的大小,再根据结果交换两个数字的位置” 这一操作的算法。冒泡排序的时间复杂度为 O(n^2)。

void BubbleSort(SqList *L)
{
    status flag = true;  /*设置标记*/
    for(int i=0; i<L->length && flag; i++)/*若flag为true则退出循环*/
    {
        flag = false;   /*初始为false*/
        for(int j=L->length-1; j>=i; j--) 
        {
            if(L->r[j] > L->r[j+1])
            {
                swap(L,j,j+1); /*交换数据*/
                flag = true;  /*设置flag为true*/
            }
        }
    }
}

2.选择排序

选择排序就是重复“从待排序的数据中寻找最小值,将其与序列最左边的数字进行交换”这一操作的算法。在序列中寻找最小值时使用的是线性查找。选择排序的时间复杂度也和冒泡排序的一样,都为 O(n^2)。

void SelectSort(SqList *L)
{
    int min;
    for (int i=1; i<L->length; i++)
    {
        min = i;    /*将当前下标定义为最小值下标*/
        for (int j=i+1; j<=L->length; j++) /*循环之后的数据*/
        {
            if(L->r[min] > L->r[j]) /*如果小于当前最小值*/
                min = j;     /*赋值给最小值*/
        }
        if(i != min)    /*最小值不等于i,则找到最小值,交换*/
            swap(L,i,min);
    }
}
3.直接插入排序

插入排序是一种从序列左端开始依次对数据进行排序的算法。在排序过程中,左侧的数据陆续归位,而右侧留下的就是还未被排序的数据。插入排序的思路就是从右侧的未排序区域内取出一个数据,然后将它插入到已排序区域内合适的位置上。时间复杂度和冒泡排序的一样,都为 O(n^2)。

void InsertSort(SqList *L)
{
    for(int i=2; i<=L->length; i++)
    {
        if(L->r[i] < L->r[i-1])  /*将L->r[i]插入有序表*/
        {
            L-r[0] = L->r[i];    /*设置哨兵*/
            for(int j=i-1; L-r[j] > L->r[0]; j--)
                L->r[j+1] = L->r[j];/*记录后移*/
            L->r[j+1] = L->r[0]; /*插入到正确位置*/
        }
    }
}

4.希尔排序

希尔排序(shell sort)是插入排序的一种改进版本,通过把增量分组,对每组进行直接插入排序,随着增量逐渐缩小直到1为止,算法才结束。

void ShellSort(SqList *L)
{
    int increment = L->length;
    do
    {
        increment = increment/3+1;  /*增量序列*/
        for(int i=increment+1; i<L->length; i++)
        {
            if(L->r[i] < L->r[i-increment])
            {   /*L->r[i]插入有序表*/
                L-r[0] = L->r[i];
                for(int j=i-increment; j>0&&L->r[0] < L->r[j]; j-=increment)
                    L->r[j+increment] = L->r[j]; /*记录后移,查找插入位置*/
                L->r[j+increment] = L->r[0];
            }
        }
    }
    while(increment > 1);
}

5.堆排序

堆排序采用数据结构中的堆,堆排序需要将n个数据存进堆里,所需时间为 O(nlogn),无论最好、最坏和平均时间复杂度均为O(nlogn)。堆排序的运行时间比之前讲到的冒泡排序、选择排序、插入排序的时间O(n^2) 都要短。每个结点的值都大于或等于其左右孩子结点的值,称为大根堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小根堆。

void HeapSort(SqList *L)
{
    int i;
    for(i=L->length/2; i>0;i--)  /*把L中的r构建成一个大根堆*/
    {
        HeapAdjust(L,i,L->length);
    }
    for(i=L->length;i>1;i--)
    {
        swap(L,1,i);    /*将堆顶记录和当前未经排序子序列的最后一个记录交换*/
        HeapAdjust(L,1,i-1); /*重新调整为大根堆*/
    }
}
void HeapAdjust(SqList *L, int s, int m)
{
    int temp;
    temp = L->r[s];
    for(int j=2*s; j<=m; j*=2)  /*沿关键字较大的孩子结点向下帅选*/
    {
        if(j<m && L->r[j] < L->r[j+1])
            ++j;    /*j是关键字中较大的记录的下标*/
        if(temp>=L->r[j])
            break;
        L->r[s] = L->r[j];  
        s = j;
    }
    L->r[s] = temp;  /*插入值*/
}

6.归并排序

归并排序算法会把序列分成长度相同的两个子序列,当无法继续往下分时(也就是每个子序列中只有一个数据时),就对子序列进行归并。归并指的是把两个排好序的子序列合并成一个有序序列。该操作会一直重复执行,直到所有子序列都归并为一个整体为止。合并这种含有多个数字的子序列时,要先比较首位数字,再移动较小的数字。无论最好、最坏和平均时间复杂度均为O(nlogn)。

/*非递归实现归并排序*/
void MergeSortByNonRecursive(SqList *L)
{
    int *TR = (int *)malloc(L->length * sizeof(int)); /*申请额外空间*/
    int k=1;
    while(k<L->length)
    {
        MergePass(L->r,TR,k,L->length);
        k=2*k;  /*子序列长度加倍*/
        MergePass(TR,L->r,k,L->length);
        k=2*k;  /*子序列长度加倍*/
    }
}
void MergePass(int SR[],int TR[],int s, int n) /*将SR[]中相邻长度为s的子序列两两归并到TR[]*/
{
    int i=1;
    while(i < n-2*s+1)
    {
        Merge(SR,TR,i,i+s-1,i+2*s-1);   /*两两归并*/
        i = i+2*s;
    }
    if(i < n-s+1)   /*归并最后两个序列*/
        Merge(SR,TR,i,i+s-1,n);
    else
        for(int j=i;j<=n;j++)
            TR[j] = SR[j];
}

/*递归实现归并排序*/
void MergeSortByRecursive(SqList *L)
{
    MSort(L->r,L->r,1,L->length);
}
void MSort(int SR[], int TR[], int s, int t) /*将SR[s..t]归并排序为TR[s..t]*/
{
    int m;
    int TR2[MAXSIZE + 1];
    if(s == t)
        TR[s] = SR[s];
    else
    {
        m = (s+t)/2;    /*将SR[s..t]平分*/
        MSort(SR,TR2,s,m);  /*递归将SR[s..m]归并为有序的TR2[s..m]*/
        MSort(SR,TR2,m+1,t);    /*递归将SR[m+1..t]归并为有序的TR2[m+1..t]*/
        Merge(TR2,TR,s,m,t);    /*将TR2[s..m]和TR2[m+1..t]归并到TR[s..t]*/
    }
}
void Merge(int SR[], int TR[], int i, int m, int n) /*将有序的SR[i..m]和SR[m+1..n]归并到TR[i..n]*/
{
    int j,k,l;
    for(j=m+1,k=i; i<=m && j<=n; k++)   /*将SR中记录有小到大归并到TR*/
    {
        if(SR[i] < SR[j])
            TR[k] = SR[i++];
        else
            TR[k] = SR[j++];
    }
    if(i<=m)
    {
        for(l=0;l<=m-i;i++)
            TR[k+1] = SR[i+1];  /*将剩余的SR[i..m]复制到TR*/
    }
    if(j<=n)
    {
        for(l=0;l<=n-j;l++)
        {
            TR[k+1] = SR[j+1];  /*将剩余的SR[j..n]复制到TR*/
        }
    }
}

7.快速排序

快速排序算法首先会在序列中随机选择一个基准值(pivot),然后将除了基准值以外的数分为“比基准值小的数”和“比基准值大的数”这两个类别,再将其排列成【比基准小的数】基准值【比基准大的数】形式。如果数据中的每个数字被选为基准值的概率都相等,那么需要的平均运行时间为 O(nlogn)。

#define MAX_LENGTH_INSERT_SORT = 7
void QuickSort(SqList *L)
{
    QSort(L,1,L->length);
}
void QSort(SqList *L, int low, int high)
{
    int pivot;
    if((high-low) > MAX_LENGTH_INSERT_SORT)
    {
        while(low < high)
        {
            pivot = Partition(L,low,high);  /*将L->r[low..high]一分为二,并算出中心pivot*/
            QSort(L,low,pivot-1);   /*对前半部分进行递归排序*/
            low = pivot+1;  /*尾递归*/
        }
    }
    else
        InsertSort(L);
}
int Partition(SqList *L, int low, int high)
{
    int pivotKey;
    pivotKey = L->r[low];   /*用子表的第一个记录中为中心*/
    L->r[0] = pivotKey;     /*备份到L->r[0]*/
    while(low < high)   /*从表两端交替向中间扫描*/
    {
        while(low < high && L->r[high] >= pivotKey)
            high--;
        L->r[low] = L->r[high];     /*采用替换而不是交换的方式进行操作*/
        while(low < high && L->r[low] <= pivotKey)
            low++;
        L->r[high] = L->r[low];     /*采用替换而不是交换的方式进行操作*/
    }
    L->r[low] = L->r[0];    /*将中心数值替换回L->r[0]*/
    return low;     /*返回中心位置*/
}

插入排序类: 直接插入排序、 希尔排序
选择排序类: 简单选择排序、堆排序
交换排序类: 冒泡排序、快速排序
归并排序类: 归并排序

简单算法: 冒泡、简单选择、直接插入
改进算法: 希尔、堆、归并、快速

排序方法 平均情况 最好情况 最坏情况 稳定性
冒泡排序 O(n^2) O(n) O(n^2) 稳定
选择排序 O(n^2) O(n^2) O(n^2) 稳定
插入排序 O(n^2) O(n) O(n^2) 稳定
希尔排序 O(nlogn)~O(n^2) O(n^1.3) O(n^2) 不稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) 稳定
快速排序 O(nlogn) O(nlogn) O(n^2) 不稳定

三. 查找

查找表是由同一类型的数据构成的集合,分为静态查找表和动态查找表。

1. 线性查找(顺序查找)

线性查找是一种在数组中查找数据的算法,它需要从头开始不断地按顺序检查数据,因此在数据量大且目标数据靠后, 或者目标数据不存在时,比较的次数就会更多,也更为耗时。若数据量为 n,线性查找的时间复杂度便为 O(n)。

int SequentialSearch(int *a, int n, int key)
{
    int i;
    a[0] = key; /*设A[0]*/
    i = n;  /*循环从数组尾部开始*/
    while(a[i] != key)
        i--;
    return i;   /*返回0查找失败*/
}

2. 有序表查找

2.1 二分查找(折半查找)

二分查找它只能查找已经排好序的数据。二分查找通过比较数组中间的数据与目标数据的大小,可以得知目标数据是在数组的左边还是右边。二分查找利用已排好序的数组,每一次查找都可以将查找范围减半。查找范围内只剩一个数据时查找结束。它的时间复杂度为 O(logn)。

int BinarySearch(int *a, int n, int key)
{
    int low, mid,high;
    low = 1;    /*定义最低下标为首*/
    high = n;   /*定义最低下标为尾*/
    while(low <= high)
    {
        mid = (low+high)/2; /*折半*/
        if(key < a[mid])    /*若比中指小*/
            high = mid-1;
        else if(key > a[mid])   /*若比中指大*/
            low = mid + 1;
        else
            return mid; /*若相等则说明mid即是查找到的位置*/
    }
    return 0;
}

2.2 插值查找

插值查找是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,其核心就在插值公式(key-a[low])/(a[high]-a[low])。

/*在二分查找代码上修改如下*/
mid = low + (high-low) * (key-a[i])/(a[high]-a[low]); /*插值*/

2.3 斐波那契查找

对于折半查找每一次总是将序列一分为二,无论数据偏大还是偏小,很多时候这样未必合理,斐波拉契查找利用黄金分割原理来实现。斐波拉契查找平均性能好于折半查找,最坏的情况下低于折半查找。

int FibonacciSearch(int *a, int n, int key)
{
    int low,high,mid,i,key;
    low = 1;
    high = n;   /*定义最高下标首位,最低下标尾位*/
    k = 0;
    while(n > F[k] - 1)
        k++;
    for(i=n;i<F[k]-1;i++)   /*将不满的数值补全*/
        a[i] = a[n];
    
    while(low <= high)
    {
        mid = low+F[k-1]-1; /*计算当前分隔的下标*/
        if(key < a[mid])
        {
            high = mid -1;
            k=k-1;  /*数列下标减一*/
        }
        else if(key > a[mid])
        {
            low = mid+1;
            k=k-2;  /*数列下标减二*/
        }
        else
        {
            if(mid <= n)
                return mid;
            else
                return n;
        }
    }
    return 0;
}

折半查找: 进行加法与除法运算,mid=(low+high)/2
插值查找: 复杂的四则运算,mid=low+(high-low)*(key-a[low])/(a[high]-a[low])
斐波拉契查找: 加减法运算,mid=low+F[k-1]-1

3. 线性索引查找

以上几种查找方法都是基于有序的基础上进行的,往往很多时候代价很大。索引的设计就是为了加快查找速度,它是把一个关键字与它对应的记录关联的过程。所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。线性索引有:稠密索引、分块索引、倒序索引等。

3.1 稠密索引

稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项,索引项一定是按照关键码有序的排列。以上三种查找性能提升,数据集巨大查找性能下降。

3.2 分块索引

稠密索引因为索引项与数据集个数相同,所以空间代价很大,为了减少索引项个数,对数据集分块,使其分块有序,再对每一个分块建立一个索引项。分块有序满足两个条件:块内无序、块间有序。

3.3 倒序索引

索引项的通用结构是:次关键码,记录号表。其中记录号表存储具有相同次关键字的记录的记录号(可以是指向记录的指针或该记录的主关键字),这样的索引方法就是倒序索引。

4. 二叉排序树

也称为二叉查找树,它或者是一颗空树或者是具有以下性质的二叉树:

  • 若他的左子树不为空,则他的左子树上所有节点的值均小于它的根节点的值
  • 若他的右子树不为空,则他的右子树上所有节点的值均大于它的根节点的值
  • 它的左右子树分别为二叉排序树

5. 平衡二叉树(AVL树)

平衡二叉树是一种二叉排序树,其中每一个结点的左子树或右子树的高度差至多等于1,把二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(BF只可能是-1、0或1)。距离插入节点最近的,且平衡因子绝对值大于1的节点为根的子树,我们称为最小不平衡子树。

6. 多路查找树(B树)

多路查找树其每一个节点的孩子数可以多于两个,且每一个节点处可以存储多个元素。常见的四种特殊形式为:2-3树、2-3-4树、B树、B+树。

  • 2-3树:每一个节点都具有两个孩子或三个孩子。一个2结点包含一个元素和两个孩子(或没有孩子),一个3结点包含一小一大两元素和三个孩子(或没有孩子)
  • 2-3-4树:在2-3树上的扩展,包括一个4节点,小中大三个元素和四个孩子(或没有孩子)
  • B树:是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。结点最大的孩子数目称为B树的阶,因此2-3树是3阶B树,2-3-4树是4阶B树。
  • B+树:是B树的一种变形树,B+树的结构适合带有范围的查找

7. 散列表(哈希表)查找

散列技术最适合的求解问题是查找与给定值相等的记录,散列表是一种非常高效的查找数据结构,它避免了关键字之间反复比较的繁琐,而是直接查找,但也带来了记录之间没有任何关联的弊端。


四. 树

树的结点拥有的子树数称为结点的度,度为0的结点称为叶节点或终端结点;度不为0的结点称为分支结点或非终端结点。根节点的子树称为孩子结点,同一双亲的孩子结点之间称为兄弟结点。
森林是多颗互不相交的树的集合。树结构:根节点唯一且无双亲;叶节点无孩子但可以多个;中间节点则一个双亲多个孩子。
二叉树:是n个结点的有限集合,该集合或为空集,或由一个根节点和两颗互不相交的、分别称为根节点的左子树和右子树的二叉树组成。
特殊的二叉树:斜树、满二叉树、完全二叉树
二叉树性质:

  • 在二叉树的第i层上至多有2^(i-1)个结点
  • 深度为k的二叉树至多有2^k-1个结点
  • 对任何一颗二叉树T,若其终端结点数为n0,度为2的结点数为n2,则n0 = n2+1
  • 具有n个结点的完全二叉树的深度为(log2n)+1
  • 对一颗n个结点的完全二叉树按层序编号,对任一结点i有:i等于1为根节点;2i>n则结点i无左孩子,否则左孩子是结点2i;2i+1>n则结点i无右孩子,否则右孩子是2i+1

二叉树的遍历:

  • 前序遍历:先访问根节点,再前序遍历左子树,再前序遍历右子树
  • 中序遍历:先中序遍历根节点的左子树,再访问根节点,再中序遍历右子树
  • 后序遍历:从左到右先叶子后结点的方式遍历访问左右子树,最后访问根节点
  • 层序遍历:从第一层开始从上往下,从左到右逐个访问
  1. 树转换为二叉树
  • 加线,在所有兄弟结点之间加一条连线
  • 去线,对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其它孩子结点之间的连线
  • 层次调整,以树的根结点为中心,旋转一定角度,第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子
  1. 森林转换为二叉树
  • 把每个树转换为二叉树
  • 第一棵二叉树不动,从第二棵二叉树开始把后一颗二叉树的根节点作为前一棵二叉树的根节点的右孩子,用线连接起来
  1. 二叉树转换为树
  • 加线,若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点。。。都作为此结点的孩子,将该结点与这些右孩子结点用线连起来
  • 去线,删除原二叉树中所有结点与其右孩子结点的连线
  • 层次调整,是它结构层次分明
  1. 二叉树转换为森林
  • 从根节点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除。。。直到所有右孩子连线都删除
  • 再将每棵分离后的二叉树转换为树即可

4.1 赫夫曼树

从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称作路径长度。树的路径长度就是从树根到每一个结点的路径长度之和。带权路径长度WPL最小的二叉树称作赫夫曼树。
对于编码字符集{d1,d2…dn},各个字符出现的频率集合{w1,w2…wn},以d1,d2…dn作为叶子结点,以w1,w2…wn作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表0,右分支代表1,则从根节点到叶子结点所经过的路径分支组成的0和1的序列便是该结点对应的编码,这就是赫夫曼编码。

五. 图

图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为G(V,E),G代表一个图,V是图顶点的集合,E是图边的集合。图按照有无方向分为有向图、无向图,按照边或弧的多少分稀疏图和稠密图。
图的存储有邻接矩阵、邻接表、十字链表、邻接多重表和边集数组。

  • 图的邻接矩阵存储方式是用两个数组来表示图,一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中边或弧的信息。如图所示:

graph

  • 考虑到邻接矩阵可能会对存储空间造成极大的浪费,于是采用邻接表 - 数组与链表相结合的存储方法。如图所示:

graph

  • 十字链表整合了邻接表与逆邻接表
  • 邻接多重表优化了无向图应用中边的操作
  • 边集数组是由两个一维数组组成,一个存储顶点信息,另一个存储边的信息,这个边数组每个数据元素由一条边的起始下标、终点下标和权组成

5.1 广度优先搜索(BFS)

广度优先搜索是一种对图进行搜索的算法,广度优先搜索会优先从离起点近的顶点开始搜索。候补顶点是用“先入先出”( FIFO)的方式来管理。广度优先搜索的特征为从起点开始,由近及远进行广泛的搜索。因此,目标顶点离起点越近,搜索结束得就越快。

void BFSTraverse(GraphAdjList GL)
{
    int i;
    EdgeNode *p;
    Queue q;
    for(i=0;i<GL->numVertexes;i++)
        visted[i] = false;
    InitQueue(&q);
    for(i=0;i<GL->numVertexes;i++)
    {
        if(!visted[i])
        {
            visted[i] = true;
            printf("%c",GL->adjList[i].data);   /*打印顶点*/
            EnQueue(&q,i);
            while(!EmptyQueue(q))
            {
                DeQueue(&q,&i);
                p = GL->adjList[i].firstedge;   /*找到当前顶点边表链表头指针*/
                while(p)
                {
                    if(!visted[p->adjvex])  /*若次顶点未被访问*/
                    {
                        visted[p->adjvex] = true;
                        printf("%c",GL->adjList[p->adjvex].data);
                        EnQueue(&q,p->adjvex);  /*将次顶点插入队列*/
                    }
                    p=p->next;  /*指针指向下一邻接点*/
                }
            }
        }
    }
}

5.2 深度优先搜索(DFS)

深度优先搜索会沿着一条路径不断往下搜索直到不能再继续为止,然后再折返,开始搜索下一条候补路径。候补顶点是用“后入先出”( LIFO)的方式来管理。广度优先搜索选择的是最早成为候补的顶点,因为顶点离起点越近就越早成为候补,所以会从离起点近的地方开始按顺序搜索;而深度优先搜索选择的则是最新成为候补的顶点,所以会一路往下,沿着新发现的路径不断深入搜索。

void DFS(GraphAdjList GL, int i)  /*邻接表的深度优先递归*/
{
    EdgeNode *p;
    visited[i] = true;
    printf("%c",GL->adjList[i].data);   /*打印顶点*/
    p = GL->adjList[i].firstedge;
    while(p)
    {
        if(!visted[p->adjvex])
            DFS(GL,p->adjvex); /*对访问的邻接顶点递归调用*/
        p = p->next;
    }
}
void DFSTraverse(GraphAdjList GL)   /*邻接表的深度遍历*/
{
    int i;
    for(i=0;i<GL->numVertexes;i++)
        visted[i] = false;  /*初始所有顶点都是未访问状态*/
    for(i=0;i<GL->numVertexes;i++)
        if(!visted[i])  /*对未访问过的顶点调用DFS,若是连通图,只会调用一次*/
            DFS(GL,i);
}

5.3. 最小生成树

构造连通网的最小代价生成树称为最小生成树,它是针对带权的连通无向图问题(即在v个顶点中找v-1条边,使得总权值最小)。最小生成树可应用于电路设计、网络设计等。
最小生成树中常见的两种算法Prim算法、Kruskal算法都利用了切分定理思想。
切分定理(Cut Property):如果一个边的两个端点属于切分(把图中的结点分为两部分,成为一个切分)不同的两边,则这个边称为横切边。在一张图中给定任意切分,横切边中权值最小的边必然属于最小生成树中的一条边。

5.3.1 Prim算法

void MiniSpanTree_Prim(MGraph G)
{
    int min,i,j,k;
    int adjvex[MAXVEX]; /*保存相关顶点下标*/
    int lowcost[MAXVEX];    /*保存相关顶点间边的权值
    lowcost[0] = 0; /*初始化第一个权值为0,即v0加入生成树*/
    adjvex[0] = 0;  /*初始化第一个顶点下标为0*/
    for(i=1;i<G.numVertexes;i++)    /*循环除下标为0外的全部顶点*/
    {
        lowcost[i] = G.arc[0][i];   /*将v0顶点与之有边的权值存入数组*/
        adjvex[i] = 0;  /*初始化都为v0的下标*/
    }
    for(i=1;i<G.numVertexes;i++)
    {
        min = INFINITY; /*初始化最小权值为无穷*/
        j=1,k=0;
        while(j<G.numVertexes)  /*循环全部顶点*/
        {
            if(lowcost[j] != 0 && lowcost[j] < min)
            {
                min = lowcost[j];   /*让当前权值称为最小值*/
                k = j;  /*将当前最小值下标存入k*/
            }
            j++;
        }
        printf("(%d,%d)",adjvex[k],k);  /*打印当前顶点边中权值最小值*/
        lowcost[k] = 0; /*将当前顶点的权值设为0,表示该顶点完成任务*/
        for(j=1;j<G.numVertexes;j++)    /*循环所有顶点*/
        {
            if(lowcost[j] != 0 && G.arc[k][j] < lowcost[j])
            {   /*若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值*/
                lowcost[j] = G.arc[k][j];   /*将较小权值存入lowcost*/
                adjvex[j] = k;  /*将下标为k的顶点存入adjvex*/
            }
        }
    }
}

5.3.2 Kruskal算法

typedef struct
{
    int begin;
    int end;
    int weight;
}Edge;
void MiniSpanTree_Kruskal(MGraph G)
{
    int i,n,m;
    Edge edges[MAXEDGE];    /*定义边集数组
    int parent[MAXVEX];     /*定义数组用来判断边与边是否形成回路*/

    for(i=0;i<G.numVertexes;i++)
        parent[i] = 0;  /*初始化数组为0*/
    for(i=0;i<G.numVertexesli++)
    {
        n = Find(parent,edges[i].begin);
        m = Find(parent,edges[i].end);
        if(n != m)  /*n不等于m,说明没有与现有生成树形成环路*/
        {
            parent[n] = m;  /*将该边的尾结点放入下标为起点的parent中*/
            printf("(%d,%d) %d", edges[i].begin, edges[i].end, edges[i].weight);
        }
    }
}
int Find(int *parent, int f)    /*查找连线顶点的尾部下标*/
{
    while(parent[f] > 0)
        f = parent[f];
    return f;
}

5.4 最短路径

对于网图来说最短路径是指两顶点之间经过的边上权值之和最小的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点

5.4.1 狄克斯特拉算法

求解最短路径问题的算法,使用它可以求得从起点到终点的路径中权重总和最小的那条路径路径。首先设置各个顶点的初始权重: 起点为 0,其他顶点为无穷大(∞)。从候补顶点中选出权重最小的顶点。确定了最短路径,移动到顶点。比起需要对所有的边都重复计算权重和更新权重的贝尔曼 - 福特算法,狄克斯特拉算法多了一步选择顶点的操作,这使得它在求最短路径上更为高效。将图的顶点数设为 n、边数设为 m,那么如果事先不进行任何处理,该算法的时间复杂度就是 O( n2)。不过,如果对数据结构进行优化,那么时间复杂度就会变为O(m +nlogn)。

typedef int Pathmatirx[MAXVEX]; /*用于存储最短路径下标的数组*/
typedef int ShortPathTable[MAXVEX]; /*用于存储各点最短路径的权值和*/
void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx *p, ShortPathTable *d)
{
    int v,w,k,min;
    int final[MAXVEX];  /*final[w]=1表示顶点v0至vw的最短路径*/
    for(v=0;v<G.numVertexes;v++)    /*初始化数据*/
    {
        final[v] = 0;   /*全部顶点初始化为未知最短路径状态*/
        (*d)[v] = G.marirx[v0][v];  /*将与v0点有连线的顶点加上权值*/
        (*p)[v] = 0;    /*初始化路径数组p为0*/
    }
    (*d)[v0] = 0;
    final[v0] = 1;
    /*开始主循环每次求得v0到某个v顶点的最短路径*/
    for(v=1;v<G.numVertexes;v++)
    {
        min = INFINITY; /*当前已知离v0顶点的最近距离*/
        for(w=0;w<.numVertexes;w++) /*寻找离v0最近的顶点*/
        {
            if(!final[w] && (*d)[w] < min)
            {
                k = w;
                min = (*d)[w];  /*w顶点离v0顶点更近*/
            }
        }
        final[k] = 1;   /*将目前找到的最近的距离置为1*/
        for(w=0;w<G.numVertexes;w++)    /*修正当前最短路径及距离*/
        {
            /*如果经过v顶点的路径比现在这条路径的长度短*/
            if(!final[w] && (min+G.matirx[k][w] < (*d)[w]))
            {
                /*说明找到了更短的路径,修正d[w],p[w]*/
                (*d)[w] = min + G.matrix[k][w]; /*修改当前路径长度*/
                (*p)[w] = k;
            }
        }
    }
}

5.4.2 弗洛伊德算法

typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
void ShortestPath_Floyd(MGraph G, Pathmatirx *p, ShortPathTable *d)
{
    int v,w,k;
    for(v=0;v<G.numVertexes;v++)    /*初始化d与p*/
    {
        for(w=0;w<G.numVertexes;w++)
        {
            (*d)[v][w] = G.matirx[v][w];    /*D[v][w]值即为对应点间的权值*/
            (*p)[v][w] = w; /*初始化p*/
        }
    }
    for(k=0;k<G.numVertexes;k++)
    {
        for(v=0;v<.numVertexes;v++)
        {
            for(w=0;w<.numVertexes;w++)
            {
                if((*d)[v][w] > (*d)[v][k] + (*d)[k][w])
                {
                    /*如果经过下标为k顶点路径比原两点间路径更短*/
                    /*将当前两点间权值设为更小的一个*/
                    (*d)[v][w] = (*d)[v][k] + (*d)[k][w];
                    (*p)[v][w] = (*p)[v][k];    /*路径设置经过下标为k的顶点*/
                }
            }
        }
    }
}

References:

  • 《我的第一本算法书》
  • 《大话数据结构》
原创文章 38 获赞 13 访问量 4033

猜你喜欢

转载自blog.csdn.net/qq_36287943/article/details/104144401
今日推荐