数据结构(学习版)

考纲

(一)数据结构部分;

  1. 线性表
  2. 栈、队列、数组
  3. 查找和内部排序
  4. 树和图

(二)计算机算法设计部分:

  1. 递归与分省策路2、回溯法5
  2. 贪心算法4、分支限界法6、动态规划3
  3. 算法设计中的数据结构运用

(三)程序设计基础(C或C++)部分:

  1. 基本数据类型、各种运算符和表达式、基本控制结构。
  2. 数组的定义、数组元素的引用、数组的初始化,掌握与字符串相关的库函数
  3. 函数的定义语法,函数调用中参数的传递机制:局部和全局变量的有效范围
  4. 结构体类型变量的定义、引用、初始化方法,结构体数组的定义、初始化和应用,共同体变量的定义和使用方法。
  5. 地址和指针的基本概念,如何使用指针来处理数组、字符串以及结构体,函数指针的基本概念以及使用。
  6. 文件的定义以及对文件进行的各种操作的库函数。

数据结构

第二章  线性表

1.顺序存储

1.1顺序表

顺序表的特点是表中元素的逻辑顺序与其存储的物理顺序相同。

1.1.1静态分配
#define MaxSize 50 //定义线性表的最大长度
typedef struct{
    ElemType data[MaxSize]; //顺序表的元素
    int length; //顺序表的当前长度
)SqList; //顺序表的类型定义
1.1.2动态分配
#define Initsize 100 //表长度的初始定义
typedef struct{
    ElemType *data; //指示动态分配数组的指针
    int MaxSize,length; //数组的最大容量和当前个数
}SeqList; //动态分配数组顺序表的类型定义
1.2顺序表的基本操作
1.2.1初始化操作
//静态分配

//sqList L; //声明一个顺序表
void Initlist(SqList &L)(
    L.length=0; //顺序表初始长度为0
)

静态分配在声明一个顺序表时,就已为其分配了数组空间,因此初始化时只需将顺序表的当前长度设为0。

//动态分配

void Initlist(SeqList &L){
    L.data=(ElemType *)malloc(MaxSize*sizeof(ElemType));//分配存储空间
    L.length=0; //顺序表初始长度为0
    L.MaxSize=Initsize; //初始存储容量
)

动态分配的初始化为顺序表分配一个预定义大小的数组空间,并将顺序表的当前长度设为0。
MaxSize指示顺序表当前分配的存储空间大小,一旦因插入元素而空间不足,就进行再分配。
 

1.2.2插入操作O(n)
bool ListInsert(Sqlist & L,int i,ElemType e){
    if(i<1||i>L.length+1) //判断i的范围是否有效
        return false;
    if(L.length>=MaxSize) //当前存储空间已满,不能插入
        return false;
    for(int j=L.length;j>=i;j--) //将第1个元素及之后的元素后移
        L.data[j]=L.data[j-1];
    L.data[i-1]=e;        //在位置i处放入e
    L.length++;           //线性表长度加1
    return true;
}

在顺序表L的第i(1<=i<=L.length+1)个位置插入新元素e。若i的输入不合法,则返回 false,表示插入失败;否则,将第i个元素及其后的所有元素依次往后移动一个位置,腾出一个空位置插入新元素 e,顺序表长度增加1,插入成功,返回 true。
 

1.2.3删除操作O(n)
bool ListDelete(SqList 6L,int i,ElemType &e){
    if(i<1 || i>L.length) //判断i的范围是否有效
        return false;
    e=L.data[i-1]; //将被删除的元素赋值给e
    for(int j=i;j<L.length;j++) //将第1个位置后的元素前移
        L.data[j-1]=L.data[j];
    L.length--;    //线性表长度减1
    return true;
}

删除顺序表L中第i(1<=i<=L.length)个位置的元素,用引用变量e返回。若i的输入不合法,则返回false;否则,将被删元素赋给引用变量e,并将第i+1个元素及其后的所有元素依次往前移动一个位置,返回true。

1.2.4查找操作(按值查找)O(n)
int LocateElem(SqList L,ElemType e){
    int i;
    for(i=0;i<L.length;i++)
        if(L.data[i]==e)
            return i+1;     //下标为i的元素值等于e,返回其位序i+1
    return 0;               //退出循环,说明查找失败
}

2链式存储 

2.1单链表

利用单链表可以解决顺序表需要大量连续存储单元的缺点,但附加的指针域,也存在浪费存储空间的缺点。由于单链表的元素离散地分布在存储空间中,因此是非随机存取的存储结构,即不能直接找到表中某个特定结点。查找特定结点时,需要从表头开始遍历,依次查找。

头结点和头指针的关系:不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。
引入头结点后,可以带来两个优点:
①由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
② 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
 

2.1.1单链表初始化
带头结点的单链表初始化时,需要创建一个头结点,并让头指针指向头结点,
头结点的 next 域初始化为 NULL。
bool Initlist(LinkList &L){         //带头结点的单链表的初始化
    L=(LNode*)malloc(sizeof(LNode));           //创建头结点
    L->next=NULL;             //头结点之后暂时还没有元素结点
    return true;
}
不带头结点的单链表初始化时,只需将头指针L初始化为NULL。
bool InitList(LinkList &L){ //不带头结点的单链表的初始化
    L=NULL;
    return true;
}
2.1.2求表长操作

求表长操作是计算单链表中数据结点的个数,需要从第一个结点开始依次访问表中每个结
点,为此需设置一个计数变量,每访问一个结点,其值加1,直到访问到空结点为止。

int Length(LinkList L){
    int len=0;          //计数变量,初始为0
    LNode *p=L;	
    while(p->next!=NULL){
        p=p->next;
        len++; //每访问一个结点,计数加1
    }
    return len;
}

求表长操作的时间复杂度为O(n)。另需注意的是,因为单链表的长度是不包括头结点的,因
此不带头结点和带头结点的单链表在求表长操作上会略有不同

2.1.3按序号查找结点

从单链表的第一个结点开始,沿着 next域从前往后依次搜索,直到找到第i个结点为止,
则返回该结点的指针;若i小于单链表的表长,则返回NULL。

LNode *GetElem(LinkList L,int i){
    LNode *p=L;           //指针p指向当前扫描到的结点
    int j=0;                 //记录当前结点的位序,头结点是第0个结点
    while(p!=NULL&&j<i){         //循环找到第1个结点
        p=p->next;
        j++;
    }
    return p;             //返回第1个结点的指针或NULL
}

按序号查找操作的时间复杂度为O(n)。

2.1.4按值查找表结点

从单链表的第一个结点开始,从前往后依次比较表中各结点的数据域,若某结点的data域
等于给定值e,则返回该结点的指针:若整个单链表中没有这样的结点,则返回NULL。

LNode *LocateElem(LinkList L, ElemType e) {
    LNode *p = L->next;
    while (p!= NULL && p->data!= e) {  
        p = p->next;
    }
    return p;  // 找到后返回该节点指针,否则返回 NULL
}


按值查找操作的时间复杂度为O(n)。
 

2.1.5插入结点操作

首先查找第i-1个结点,假设第i-1个结点为*p,然后令新结点*s 的指针域指向*p的后
继,再令结点*p的指针域指向新插入的结点*s。

bool ListInsert(LinkList *L, int i, ElemType e) {
    LNode *p = *L;  // 指针 p 指向当前扫描到的结点
    int j = 0;          // 记录当前结点的位序,头结点是第 0 个结点
    while (p!= NULL && j < i - 1) {        // 循环找到第 i - 1 个结点
        p = p->next;
        j++;
    }

    if (p == NULL)
        return false;  // i 值不合法

    LNode *s = (LNode *)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;        // 图 2.5 中操作步骤①
    p->next = s;           // 图 2.5 中操作步骤②
    return true;
}


插入时,①和②的顺序不能颠倒,否则,先执行p->next=s 后,指向其原后继的指针就不
存在了,再执行s->next=p->next时,相当于执行了s->next=s,显然有误。本算法主要的
时间开销在于查找第i-1个元素,时间复杂度为O(n)。若在指定结点后插入新结点,则时间复杂
度仅为O(1)。需注意的是,当链表不带头结点时,需要判断插入位置i是否为1,若是,则要做
特殊处理,将头指针L指向新的首结点。当链表带头结点时,插入位置i为1时不用做特殊处理。
扩展:对某一结点进行前插操作。
前插操作是指在某结点的前面插入一个新结点,后插操作的定义刚好与之相反。在单链表插
入算法中,通常都采用后插操作。以上面的算法为例,先找到第i-1个结点,即插入结点的前驱,
再对其执行后插操作。由此可知,对结点的前插操作均可转化为后插操作,前提是从单链表的头
结点开始顺序查找到其前驱结点,时间复杂度为O(n)。
此外,可采用另一种方式将其转化为后插操作来实现,设待插入结点为*s,将*s插入到*p
的前面。我们仍然将*s插入到*p的后面,然后将p->data与s->data 交换,这样做既满足逻
辑关系,又能使得时间复杂度为O(1)。该方法的主要代码片段如下

s->next=p->next;   //修改指针域,不能颠倒
p->next=s;
temp=p->data;      //交换数据域部分
p->data=s->data;	
s->data=temp;
2.1.6删除节点操作

假设结点*p 为找到的被删结点的前驱,为实现这一操作后的逻辑关系的变化,仅需修改*p的指针域,将*p的指针域 next 指向*g的下一结点,然后释放*q的存储空间。

bool ListDelete(LinkList &L, int i, ElemType &e) {
    LNode *p = L;// 指针 p 指向当前扫描到的结点
    int j = 0;  // 记录当前结点的位序,头结点是第 0 个结点
    
    while (p!= NULL && j < i - 1) {  
        p = p->next;
        j++;
    }

    if (p == NULL || p->next == NULL)  
        return false;  // i 值不合法

    LNode *q = p->next;	   // 令 q 指向被删除结点
    e = q->data;           // 用 e 返回元素的值
    p->next = q->next;     // 将 *q 结点从链中“断开”
    free(q);               // 释放结点的存储空间
    return true;  
    
}

同插入算法一样,该算法的主要时间也耗费在查找操作上,时间复杂度为O(n)。当链表不带头结点时,需要判断被删结点是否为首结点,若是,则要做特殊处理,将头指针L指向新的首结点。当链表带头结点时,删除首结点和删除其他结点的操作是相同的。

扩展:删除结点*p.
要删除某个给定结点*p,通常的做法是先从链表的头结点开始顺序找到其前驱,然后执行删
除操作。其实,删除结点*p的操作可用删除*p的后继来实现,实质就是将其后继的值赋予其自
身,然后再删除后继,也能使得时间复杂度为O(1)。该方法的主要代码片段如下:

q=p->next;       //令q指向*p的后继结点
p->data=p->next->data;  //用后继结点的数据域覆盖
p->next=q->next;         //将*q结点从链中“断开”
free(q);              //释放后继结点的存储空间
2.1.7头插法建立单链表
// 逆向建立单链表
LinkList List_HeadInsert(LinkList L) {
    LNode *s; 
    int x; // 设元素类型为整型

    /* 创建头结点 */
    L = (LNode*)malloc(sizeof(LNode));
    L->next = NULL;     //初始为空链表

    scanf("%d", &x);        //输入结点的值
    while (x!= 9999) {
        /* 输入 9999 表示结束 */
        s = (LNode*)malloc(sizeof(LNode));
        s->data = x;
        s->next = L->next;
        L->next = s; // 将新结点插入表中,L 为头指针
        scanf("%d", &x);
    }
    // 执行 free(q)的作用是由系统回收一个 LNode 型结点,回收后的空间可供再次生成结点时用。
    return L;
}

采用头插法建立单链表时,读入数据的顺序与生成的链表中元素的顺序是相反的,可用来实
现链表的逆置。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。

2.1.8尾插法建立单链表
// 正向建立单链表
LinkList list_TailInsert(LinkList &L) {  
    int x;  // 设元素类型为整型
   
    L = (LNode*)malloc(sizeof(LNode)); /* 创建头结点 */

    LNode *s, *r = L;        // r 为表尾指针

    scanf("%d", &x);            //输入结点的值
    while (x!= 9999) {        // 输入 9999 表示结束
        s = (LNode *)malloc(sizeof(LNode));
        s->data = x;
        r->next = s;
        r = s;                   // r 指向新的表尾结点
        scanf("%d", &x);  
    }
    r->next = NULL;           // 尾结点指针置空
    return L;
}

因为附设了一个指向表尾结点的指针,所以时间复杂度和头插法的相同

3.1双链表
3.1.1插入操作
s->next=p->next; //将结点*s插入到结点*p之后
p->next->prior=s
s->prior=p;
p->next=s;
3.1.2删除操作

p->next=q->next; //图2.11 中步骤①
q->next->prior=p; //图 2.11 中步骤②
free(q); //释放结点空间
3.2循环链表
3.3静态列表

3.顺序表和链表的比较

1.存取(读/写)方式

顺序表可以顺序存取,也可以随机存取链表只能从表头开始依次顺序存取。例如在第i个位置上执行存取的操作,顺序表仅需一次访问,而链表则需从表头开始依次访问i次。

2.逻辑结构与物理结构

采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的。

3.查找、插入和删除操作

对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log?n)
对于按序号查找,顺序表支持随机访问,时间复杂度仅为O(1),链表平均时间复杂度为O(n)。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需修改相关结点的指针域即可。

4.空间分配

顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出动态存储分配虽然存储空间可以扩充,但需要移动大量元素导致操作效率降低,而且内存中没有更大块的连续存储空间则会导致分配失败
链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。此外,由于链表的每个结点都带有指针域,因此存储密度不够大。

在实际中应该怎样选取存储结构呢?

1.基于存储的考虑

难以估计线性表的长度或存储规模时,不宜采用顺序表;
链表不用事先估计存储规模,但链表的存储密度较低,显然链式存储结构的存储密度是小于1的。

2.基于运算的考虑

在顺序表中按序号访问a,的时间复杂度为O(1),而链表中按序号访问的时间复杂度为O(n),
因此若经常做的运算是按序号访问数据元素,则显然顺序表优于链表。
在顺序表中进行插入、删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且
表较长时,这一点是不应忽视的;在链表中进行插入、删除操作时,虽然也要找插入位置,但操
作主要是比较操作,从这个角度考虑显然后者优于前者。

3.基于环境的考虑

顺序表容易实现,任何高级语言中都有数组类型;链表的操作是基于指针的,相对来讲,前
者实现较为简单,这也是用户考虑的一个因素。
总之,两种存储结构各有长短,选择哪一种由实际问题的主要因素决定。通常较稳定的线性
表选择顺序存储,而频繁进行插入、删除操作的线性表(即动态性较强)宜选择链式存储
 

顺序表的主要优点:
①可进行随机访问,即可通过首地址和元素序号可以在O(1)时间内找到
指定的元素;
②存储密度高,每个结点只存储数据元素。
顺序表的缺点也很明显:
①元素的插入和删除需要移动大量的元素,插入操作平均需要移动n/2个元素,删除操作平均需要移动(n-1)/2个元素;
②顺序存储分配需要一段连续的存储空间,不够灵活。

第三章  栈、队列和数组

1.栈

1.1顺序栈
1.1.1顺序栈的实现
#define MaxSize 50    // 定义栈中元素的最大个数
typedef struct {
    Elemtype data[MaxSize];  // 存放栈中元素
    int top;  // 栈顶指针
} SqStack;  
1.1.2顺序栈的基本操作
(1)初始化
void Initstack(SqStack &S) {
    s.top = -1;  // 初始化栈顶指针
}
----------------------------------------------------------------------------------

(2)判栈空
bool StackEmpty(SqStack S) {
    if (s.top == -1)  // 栈空
        return true;
    else  // 不空
        return false;
}
----------------------------------------------------------------------------------

(3)进栈
bool Push(SqStack &S, ElemType x) {
    if (S.top == MaxSize - 1)  // 栈满,报错
        return false;
    S.data[++S.top] = x;  // 指针先加 1,再入栈
    return true;
}
----------------------------------------------------------------------------------

(4)出栈
bool Pop(SqStack &S, ElemType &x) {
    if (S.top == -1)  // 栈空,报错
        return false;
    x = S.data[S.top--];  // 先出栈,指针再减 1
    return true;
}
----------------------------------------------------------------------------------

(5)读栈顶元素
bool GetTop(SqStack S, ElemType &x) {
    if (S.top == -1)  // 栈空,报错
        return false;
    x = S.data[S.top];  // x 记录栈顶元素
    return true;
}
//仅为读取栈顶元素,并没有出栈操作,因此原栈顶元素依然保留在栈中。
1.2链栈
1.21栈的链式存储结构
typedef struct Linknode {
    ElemType data;  // 数据域
    struct Linknode *next;  // 指针域
} Listack;  // 栈类型定义
1.3共享栈

2.队列

2.0队列的顺序存储
#define MaxSize 50
typedef struct {
    // 定义队列中元素的最大个数
    ElemType data[MaxSize];
    // 用数组存放队列元素
    int front, rear;
    // 队头指针和队尾指针
} SqQueue;
2.1循环队列
2.1.1循环队列的操作
(1)初始化
void InitQueue(SqQueue &Q) {
    Q.rear = Q.front = 0;  // 初始化队首、队尾指针
}
---------------------------------------------------------------------

(2)判队空
bool isEmpty(SqQueue Q) {
    if (Q.rear == Q.front)  // 队空条件
        return true;
    else
        return false;
}
---------------------------------------------------------------------

(3)入队
bool EnQueue(SqQueue &Q, ElemType x) {
    if ((Q.rear + 1) % MaxSize == Q.front)  // 队满则报错
        return false;
    Q.data[Q.rear] = x;
    Q.rear = (Q.rear + 1) % MaxSize;  // 队尾指针加 1 取模
    return true;
}
---------------------------------------------------------------------

(4)出队
bool DeQueue(SqQueue &Q, ElemType &x) {
    if (Q.rear == Q.front)  // 队空则报错
        return false;
    x = Q.data[Q.front];
    Q.front = (Q.front + 1) % MaxSize;  // 队头指针加 1 取模
    return true;
}
2.2链式队列
2.2.1队列的链式存储
typedef struct LinkNode {  // 链式队列结点
    ElemType data;
    struct LinkNode *next;
} LinkNode;

typedef struct {  // 链式队列
    LinkNode *front, *rear;  // 队列的队头和队尾指针
} LinkQueue;

// 不带头结点时,当 Q.front==NULL 且 Q.rear==NULL 时,链式队列为空。

2.2.2链式队列的基本操作

(1)初始化
void InitQueue(LinkQueue &Q) {  // 初始化带头结点的链队列
    Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));  // 建立头结点
    Q.front->next = NULL;  // 初始为空
}
----------------------------------------------------------------------------

(2)判队空
bool IsEmpty(LinkQueue Q) {
    if (Q.front == Q.rear)  // 判空条件
        return true;
    else
        return false;
}
----------------------------------------------------------------------------

(3)入队
void EnQueue(LinkQueue &Q, ElemType x) {
    LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));  // 创建新结点
    s->data = x;
    s->next = NULL;
    Q.rear->next = s;  // 插入链尾
    Q.rear = s;  // 修改尾指针
}
----------------------------------------------------------------------------

(4)出队
bool DeQueue(LinkQueue &Q, ElemType &x) {
    if (Q.front == Q.rear)
        return false;  // 空队
    LinkNode *p = Q.front->next;
    x = p->data;
    Q.front->next = p->next;
    if (Q.rear == p)
        Q.rear = Q.front;  // 若原队列中只有一个结点,删除后变空
    free(p);
    return true;
}
2.3双端队列

3.数组

3.1一维数组
3.2多维数组:压缩存储、稀疏矩阵

总结

第五章  树与二叉树

1.二叉树

1.1概念:定义、存储结构
1.1.1基本术语

结点的度和树的度。

能往下分几个,度就是几。一般的数度为2

结点的深度、高度和层次。
高度:圈的层数

1.1.2树的性质

1)树的结点数n等于所有结点的度数之和加1.
2)度为m的树中第i层上至多有m-'个结点(i≥1)。
3)高度为h的m叉树至多有(m?-1)/(m-1)个结点。
4)度为m、具有n个结点的树的最小高度h为[log。(n(m-1)+1)1.
5)度为m、具有n个结点的树的最大高度h为n-m+1。
 

1.2操作
1.2.1三种遍历

1.先序遍历(PreOrder)根左右

void PreOrder(BiTree T) {
    if (T!= NULL) {  // 若树不为空
        visit(T);  // 访问根结点
        PreOrder(T->lchild);  // 递归遍历左子树
        PreOrder(T->rchild);  // 递归遍历右子树
    }
}

2.中序遍历(InOrder)左根右

void InOrder(BiTree T) {
    if (T!= NULL) {  // 若树不为空
        InOrder(T->lchild);  // 递归遍历左子树
        visit(T);  // 访问根结点
        InOrder(T->rchild);  // 递归遍历右子树
    }
}

3.后序遍历(PostOrder)左右根

void PostOrder(BiTree T) {
    if (T!= NULL) {  // 若树不为空
        PostOrder(T->lchild);  // 递归遍历左子树
        PostOrder(T->rchild);  // 递归遍历右子树
        visit(T);  // 访问根结点
    }
}

1.2.2线索二叉树
typedef struct ThreadNode {
    ElemType data;  // 数据元素
    struct ThreadNode *lchild, *rchild;  // 左、右孩子指针
    int ltag, rtag;  // 左、右线索标志
} ThreadNode, *ThreadTree;

1.3应用
1.3.1并查集
并查集的结构定义如下

#define SIZE 100
int UFSets[SIZE]; // 集合元素数组(双亲指针数组)

---------------------------------------------------------------------------------------

(1)并查集的初始化操作
void Initial(int s[]) {  // s 即为并查集
    for (int i = 0; i < SIZE; i++)  // 每个自成单元素集合
        s[i] = -1;
}
---------------------------------------------------------------------------------------

(2)并查集的 Find 操作
在并查集 s 中查找并返回包含元素 x 的树的根。
int Find(int s[], int x) {
    while (s[x] >= 0)  // 循环寻找 x 的根
        x = s[x];
    return x;  // 根的 s[]小于 0
}
判断两个元素是否属于同一集合,只需分别找到它们的根,再比较根是否相同即可。
---------------------------------------------------------------------------------------

(3)并查集的 Union 操作
求两个不相交子集合的并集。若将两个元素所在的集合合并为一个集合,则需要先找到两个
元素的根,再令一棵子集树的根指向另一棵子集树的根。
void Union(int s[], int Root1, int Root2) {
    if (Root1 == Root2) return;  // 要求 Root1 与 Root2 是不同的集合
    s[Root2] = Root1;  // 将根 Root2 连接到另一根 Root1 下面
}
Find 操作和 Union 操作的时间复杂度分别为 O(d) 和 O(1),其中 d 为树的深度。

查并集的优化

1.3.2哈夫曼树

2.树和森林

2.1概念:定义、存储结构

树的存储结构
1.双亲表示法

#define MAX_TREE_SIZE 100
typedef struct {
    ElemType data;  // 数据元素
    int parent;  // 双亲位置域
} PTNode;  // 树的结点定义

typedef struct {  // 树的类型定义
    PTNode nodes[MAX_TREE_SIZE];  // 双亲表示
    int n;  // 结点数
} PTree;  // 树中最多结点数

2.2操作

2.2.1与二叉树的转换

2.2.2遍历

2.3应用:并查集
 

第6章  图

6.1图的定义

6.2图结构的存储

6.2.1邻接矩阵法
#define MaxVertexNum 100  // 顶点数目的最大值
typedef char VertexType;  // 顶点对应的数据类型
typedef int EdgeType;  // 边对应的数据类型

typedef struct {
    VertexType vex[MaxVertexNum];  // 顶点表
    EdgeType edge[MaxVertexNum][MaxVertexNum];  // 邻接矩阵,边表
    int vexnum, arcnum;  // 图的当前顶点数和边数
} MGraph;
6.2.2邻接表法
#define MaxVertexNum 100  // 图中顶点数目的最大值
typedef struct ArcNode {
    int adivexi;  // 该弧所指向的顶点的位置
    struct ArcNode *nextarc;  // 指向下一条弧的指针
    // InfoType info;  // 网的边权值
} ArcNode;  // 边表结点

typedef struct VNode {  // 顶点表结点
    VertexType data;  // 顶点信息
    ArcNode *firstarc;  // 指向第一条依附该顶点的弧的指针
} VNode, AdjList[MaxVertexNum];

typedef struct {
    AdjList vertices;  // 邻接表
    int vexnum, arcnum;  // 图的顶点数和弧数
} ALGraph;  // ALGraph 是以邻接表存储的图类型
6.2.3邻接多重表
6.2.4十字链表
图的四种存储方式的总结
 

6.3图的遍历

广度优先遍历

广度优先搜索算法的伪代码如下:
bool visited[MAX_VERTEX_NUM];  // 访问标记数组
void BFSTraverse(Graph G) {              //对图g进行广度优先遍历
    for (i = 0; i < G.vexnum; ++i)
        visited[i] = FALSE;  // 访问标记数组初始化
    InitQueue(Q);  // 初始化辅助队列 Q

    for (i = 0; i < G.vexnum; ++i)  // 从 0 号顶点开始遍历
        if (!visited[i])  // 对每个连通分量调用一次 BFS()
            BFS(G, i);  // 若 v 未访问过,从 v 开始调用 BFS()
}
-----------------------------------------------------------------------------------
用邻接表实现广度优先搜索的算法如下:
void BFS(ALGraph G, int i) {
    visit(i);  // 访问初始顶点 i
    visited[i] = TRUE;  // 对 i 做已访问标记
    EnQueue(Q, i);  // 顶点 i 入队

    while (!IsEmpty(Q)) {
        DeQueue(Q, v);  // 队首顶点 v 出队

        for (p = G.vertices[v].firstarc; p; p = p->nextarc) {  // 检测 v 的所有邻接点
            w = p->adjvex;
            if (visited[w] == FALSE) {
                visit(w);  // w 为 v 的尚未访问的邻接点,访问 w
                visited[w] = TRUE;  // 对 w 做已访问标记
                EnQueue(Q, w);  // 顶点 w 入队
            }
        }
    }
}
-----------------------------------------------------------------------------------

用邻接矩阵实现广度优先搜索的算法如下:
void BFS(MGraph G, int i) {
    visit(i);  // 访问初始顶点 i
    visited[i] = TRUE;  // 对 i 做已访问标记
    EnQueue(Q, i);  // 顶点 i 入队

    while (!IsEmpty(Q)) {
        DeQueue(Q, v);  // 队首顶点 v 出队

        for (w = 0; w < G.vexnum; w++)  // 检测 v 的所有邻接点
            if (visited[w] == FALSE && G.edge[v][w] == 1) {
                visit(w);  // w 为 v 的尚未访问的邻接点,访问 w
                visited[w] = TRUE;  // 对 w 做已访问标记
                EnQueue(Q, w);  // 顶点 w 入队
            }
    }
}

// 辅助数组 visited[]标志顶点是否被访问过,其初始状态为 FALSE。在图的遍历过程
// 中,一旦某个顶点 v 被访问,就立即置 visited[i]为 TRUE,防止它被多次访问

深度优先遍历


bool visited[MAX_VERTEX_NUM];  // 访问标记数组

void DFSTraverse(Graph G) {
    for (i = 0; i < G.vexnum; i++)
        visited[i] = FALSE;  // 初始化已访问标记数组
    for (i = 0; i < G.vexnum; i++)
        if (!visited[i])
            DFS(G, i);  // 本代码中是从 v?开始遍历,对尚未访问的顶点调用 DFS()
}  // 对图 G 进行深度优先遍历
-----------------------------------------------------------------------------------
// 用邻接表实现深度优先搜索的算法如下:
void DFS(ALGraph G, int i) {
    visit(i);  // 访问初始顶点 i
    visited[i] = TRUE;  // 对 i 做已访问标记
    for (p = G.vertices[i].firstarc; p; p = p->nextarc) {  // 检测 i 的所有邻接点
        j = p->adjvex;
        if (visited[j] == FALSE)
            DFS(G, j);  // j 为 i 的尚未访问的邻接点,递归访问 j
    }
}
-----------------------------------------------------------------------------------

// 用邻接矩阵实现深度优先搜索的算法如下:
void DFS(MGraph G, int i) {
    visit(i);  // 访问初始顶点 i
    visited[i] = TRUE;  // 对 i 做已访问标记
    for (j = 0; j < G.vexnum; j++) {  // 检测 i 的所有邻接点
        if (visited[j] == FALSE && G.edge[i][j] == 1)
            DFS(G, j);  // j 为 i 的尚未访问的邻接点,递归访问 j
    }
}

6.4图的相关应用

6.4.1最小生成树

Prim 算法

Kruskal 算法

最短路径:Dijkstra 算法、Floyd 算法
拓扑排序:

bool TopologicalSort(Graph G) {
    Initstack(S);  // 初始化栈,存储入度为 0 的顶点

    int i;
    for (i = 0; i < G.vexnum; i++) {
        if (indegree[i] == 0)
            Push(S, i);  // 将所有入度为 0 的顶点进栈
    }

    // 计数,记录当前已经输出的顶点数
    int count = 0;

    while (!IsEmpty(S)) {  // 栈不空,则存在入度为 0 的顶点
        Pop(S, i);
        print[count++] = i;  // 输出顶点 i

        for (p = G.vertices[i].firstarc; p; p = p->nextarc) {
            // 将所有 i 指向的顶点的入度减 1,并且将入度减为 0 的顶点压入栈 S
            v = p->adjvex;
            if (!(--indegree[v]))
                Push(S, v);  // 入度为 0,则入栈
        }
    }

    if (count < G.vexnum)
        return false;  // 排序失败,有向图中有回路
    else
        return true;  // 拓扑排序成功
}

AOV 网
关键路径:AOE网
 

第7章  查找

7.1基本概念:静态查找、动态查找

7.2线性结构

7.2.1顺序查找
typedef struct {  // 查找表的数据结构(顺序表)
    ElemType *elem;  // 动态数组基址
    int TableLen;  // 表的长度
} SSTable;

int Search_seq(SSTable ST, ElemType key) {
    ST.elem[0] = key;  // “哨兵”
    for (int i = ST.TableLen; ST.elem[i]!= key; --i);  // 从后往前找
    return i;  // 若查找成功,则返回元素下标;若查找失败,则返回 0
}
7.2.2折半查找
int Binary_search(SSTable L, ElemType key) {
    int low = 0, high = L.TableLen - 1, mid;
    while (low <= high) {  // 当查找范围存在
        mid = (low + high) / 2;  // 取中间位置
        if (L.elem[mid] == key)
            return mid;  // 查找成功则返回所在位置
        else if (L.elem[mid] > key)
            high = mid - 1;  // 从前半部分继续查找
        else
            low = mid + 1;  // 从后半部分继续查找
    }
    return -1;  // 查找失败,返回 -1
}
7.2.3分块查找

7.3树形结构

7.3.1二叉排序树
二叉排序树的查找
二叉排序树的非递归查找算法:
BSTNode *BST_Search(BiTree T, ElemType key) {
    while (T!= NULL && key!= T->data) {  // 若树空或等于根结点值,则结束循环
        if (key < T->data)
            T = T->lchild;  // 小于,则在左子树上查找
        else
            T = T->rchild;  // 大于,则在右子树上查找
    }
    return T;
}
--------------------------------------------------------------------------------------
二叉排序树的插入
int BST_Insert(BiTree *T, KeyType k) {
    if (*T == NULL) {  // 原树为空,新插入的记录为根结点
        *T = (BiTree)malloc(sizeof(BSTNode));
        (*T)->data = k;
        (*T)->lchild = (*T)->rchild = NULL;
        return 1;          // 返回 1, 插入成功
    }
    else if (k == (*T)->data)  // 树中存在相同关键字的结点,插入失败
        return 0;
    else if (k < (*T)->data)  // 插入*T 的左子树
        return BST_Insert(&((*T)->lchild), k);
    else            // 插入*T 的右子树
        return BST_Insert(&((*T)->rchild), k);
}

-------------------------------------------------------------------------------------
二叉排序树的构造
void Creat_BST(BiTree *T, KeyType str[], int n) {
    *T = NULL;  // 初始时 T 为空树
    int i = 0;
    while (i < n) {  // 依次将每个关键字插入二叉排序树
        BST_Insert(T, str[i]);
        i++;
    }
}

7.3.2二叉平衡树

7.3.3红黑树

7.3.4B树、B+树

​7.4散列结构--散列表

​7.4.1性能分析

​7.4.2冲突处理

7.5效率指标--平均查找长度

7.5.1查找成功

7.5.2查找失败

第8章  排序

插入排序

直接插入排序

void InsertSort(ElemType A[], int n) {
    int i, j;
    for (i = 2; i <= n; i++) {  // 依次将 A[2]~A[n]插入前面已排序序列
        if (A[i] < A[i - 1]) {  // 若 A[i]关键码小于其前驱,将 A[i]插入有序表
            A[0] = A[i];  // 复制为哨兵,A[0]不存放元素
            for (j = i - 1; A[0] < A[j]; --j)  // 从后往前查找待插入位置
                A[j + 1] = A[j];  // 向后挪位
            A[j + 1] = A[0];  // 复制到插入位置
        }
    }
}

折半插入排序

void InsertSort(ElemType A[], int n) {
    int i, j, low, high, mid;
    for (i = 2; i <= n; i++) {  // 依次将 A[2]~A[n]插入前面的已排序序列
        A[0] = A[i];  // 将 A[i]暂存到 A[0]
        low = 1;
        high = i - 1;  // 设置折半查找的范围
        while (low <= high) {  // 折半查找(默认递增有序)
            mid = (low + high) / 2;  // 取中间点
            if (A[mid] > A[0])
                high = mid - 1;  // 查找左半子表
            else
                low = mid + 1;  // 查找右半子表
        }
        for (j = i - 1; j >= high + 1; --j)
            A[j + 1] = A[j];  // 统一后移元素,空出插入位置
        A[high + 1] = A[0];  // 插入操作
    }
}

希尔排序
 

void ShellSort(ElemType A[], int n) {
    // A[0]只是暂存单元,不是哨兵,当 j<=0 时,插入位置已到
    int dk, i, j;
    for (dk = n / 2; dk >= 1; dk = dk / 2)  // 增量变化(无统一规定)
        for (i = dk + 1; i <= n; ++i)
            if (A[i] < A[i - dk]) {  // 需将 A[i]插入有序增量子表
                A[0] = A[i];  // 暂存在 A[0]
                for (j = i - dk; j > 0 && A[0] < A[j]; j -= dk)
                    A[j + dk] = A[j];  // 记录后移,查找插入的位置
                A[j + dk] = A[0];  // 插入
            }
}

选择排序

简单选择排序
void SelectSort(ElemType A[], int n) {
    for (int i = 0; i < n - 1; i++) {  // 一共进行 n - 1 趟
        int min = i;  // 记录最小元素位置
        // 在 A[i…n - 1]中选择最小的元素
        for (int j = i + 1; j < n; j++)
            if (A[j] < A[min])
                min = j;  // 更新最小元素位置
        // 封装的 swap()函数共移动元素 3 次
        if (min!= i)
            swap(A[i], A[min]);
    }
}
堆排序
 
void HeapSort(ElemType A[], int len) {
    BuildMaxHeap(A, len);  // 初始建堆
    for (int i = len; i > 1; i--) {  // n - 1 趟的交换和建堆过程
        // 输出堆顶元素(和堆底元素交换)
        Swap(A[i], A[1]);
        HeadAdjust(A, 1, i - 1);  // 调整,把剩余的 i - 1 个元素整理成堆
    }
}
大根堆
void BuildMaxHeap (ElemType A[], int len) {
    for (int i = len / 2; i > 0; i--) {  // 从 i = [n/2] ~ 1,反复调整堆
        HeadAdjust(A, i, len);
    }
}

void HeadAdjust(ElemType A[], int k, int len) {
    // 函数 HeadAdjust 对以元素 k 为根的子树进行调整
    A[0] = A[k];  // A[0] 暂存子树的根结点
    for (int i = 2 * k; i <= len; i *= 2) {  // 沿 key 较大的子结点向下筛选
        if (i < len && A[i] < A[i + 1])
            i++;  // 取 key 较大的子结点的下标
        if (A[0] >= A[i])
            break;  // 筛选结束
        else {
            A[k] = A[i];  // 将 A[i] 调整到双亲结点上
            k = i;  // 修改 k 值,以便继续向下筛选
        }
    }
    A[k] = A[0];  // 被筛选结点的值放入最终位置
}

归并排序、基数排序和计数排序

计算机算法设计与分析

第2章递归与分治策略

(1)二分搜索技术

(2)大整数乘法

(3)Strassen矩阵乘法

(4)棋盘覆盖

(5)合并排序和快速排序

(6)线性时间选择

(7)最接近点对问题

(8)循环赛日程表

第3章动态规划

(1)矩阵连乘问题

(2)最长公共子序列

(3)最大子段和

(4)凸多边形最优三角剖分

(5)多边形游戏

(6)图像压缩

(7)电路布线

(8)流水作业调度

(9)背包问题

(10)最优二叉搜索树

第4章贪心算法

(1)活动安排问题

(2)最优装载问题

(3)哈夫曼编码

(4)单源最短路径

(5)最小生成树

(6)多机调度问题

第5章回溯法

(1)装载问题

(2)批处理作业调度

(3)符号三角形问题

(4)n后问题

(5)0-1背包问题

(6)最大团问题

(7)图的m 着色问题

(8)旅行售货员问题

(9)圆排列问题

(10)电路板排列问题

(11)连续邮资问题

第6章分支限界法

(1)单源最短路径问题

(2)装载问题

(3)布线问题

(4)0-1背包问题

(5)最大团问题

(6)旅行售货员问题

(7)电路板排列问题

(8)批处理作业调度问题

最后

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值