408 数据结构
文章目录
- 线性表
- 顺序表
- 静态分配
- 动态分配
- 算法设计
- 链表
- 单链表
- 双链表
- 循环链表
- 循环单链表
- 循环双链表
- 静态链表
- 算法设计
- 栈
- 顺序栈
- 共享栈
- 链式栈
- 算法设计
- 应用
- 队列
- 循环队列
- 链队列
- 算法设计
- 串
- 顺序存储
- 链式存储
- 串的模式匹配
- 树
- 二叉树
- 线索二叉树
- 树、森林
- 树、森林的存储
- 树和森林的遍历
- 算法设计
- 集合
- 并查集
- 图
- 图的存储
- 邻接矩阵法
- 邻接表法
- 十字链表法
- 邻接多重表
- 图的基本操作
- 图的遍历
- 广度优先遍历BFS
- 深度优先遍历DFS
- 最小生成树
- prim算法
- kruskal算法
- 最短路径
- BFS求单源最短路径
- Dijkstra算法
- Floyd算法
- 算法设计
线性表
顺序表
静态分配
定义
#define MaxSize 50
typedef struct{
int data[MaxSize];
int length;
}SqList;
初始化
void InitList(SqList& L){
L.length = 0;
}
按位序插入
bool ListInsert(SqList& L, int i, int e){
if(i < 1 || i > L.length + 1)
return false;
if(L.length >= MaxSize)
return false;
for(int j = L.length; j >= i; j--)
L.data[j] = L.data[j - 1];
L.data[i - 1] = e;
L.length++;
return true;
}
按位序删除
bool ListDelete(SqList& L, int i, int& e){
if(i < 1 || i > L.length)
return false;
e = L.data[i - 1];
for(int j = i; j < L.length; j++)
L.data[j - 1] = L.data[j];
L.length--;
return true;
}
按位查找
应加入一个判断,i不能小于1或大于length
int GetElem(SqList L, int i){
return L.data[i - 1];
}
按值查找
int LocateElem(SqList L, int e){
for(int i = 0; i < L.length; i++)
if(L.data[i] == e)
return i + 1;
return 0;
}
测试
int main(){
SqList L;
InitList(L);
cout << ListInsert(L,1,1) << endl; // 1
cout << ListInsert(L,2,2) << endl; // 1
cout << ListInsert(L,4,4) << endl; // 0
cout << GetElem(L, 1) << endl; // 1
cout << GetElem(L, 2) << endl; // 2
cout << LocateElem(L, 2) << endl; // 2
cout << LocateElem(L, 4) << endl; // 0
int del;
cout << ListDelete(L, 1, del) << " " << del << endl; // 1 0
cout << GetElem(L, 1) << endl; // 2
return 0;
}
🐯 注意
- 传参时,对顺序表进行修改的都要加
&
,如初始化、删除、插入
动态分配
定义
#define InitSize 50
typedef struct{
int *data;
int MaxSize;
int length;
}SqList;
初始化
void InitList(SqList& L){
// 默认最大长度
L.MaxSize = InitSize;
L.length = 0;
L.data = (int *)malloc(InitSize * sizeof(int));
// L.data = new int[InitSize];
}
增大数组长度
void IncreaseSize(SqList& L, int len){
int *p = L.data;
L.data = (int *)malloc((L.MaxSize + len) * sizeof(int));
// L.data = new int[L.MaxSize + len];
for(int i = 0; i < L.length; i++)
L.data[i] = p[i];
L.MaxSize = L.MaxSize + len;
free(p);
// delete []p;
}
使用C++语言进行分配和删除
L.data = new int[InitSize];
delete []p;
增删改查 与静态分配的一样。
🐯 注意
-
使用
malloc
free
函数时需要引入头文件cstdlib
-
可以使用C++的
new
与delete
-
动态分配只是说存储空间大小是动态的,但是元素之间的物理位置仍与逻辑位置一样。所以仍是顺序表。
算法设计
🐯 注意
- 如无特殊说明,有序指递增有序
- 在真正考试时,数据类型要写 ElemType,而不是特指某一个
1 从顺序表中删除具有最小值的元素(假设唯一)并由函数返回被删除的元素的值。空出的位置由最后一个元素填补,若顺序表为空,则显示出错误信息并退出运行。
// ElemType
bool Del_Min(SqList &L, int &value){
if(L.length == 0) return false;
int pos = 0;
for(int i = 1; i < L.length; i++){
if(L.data[i] < L.data[pos])
pos = i; // 更新最小值下标。 可以不记录最小值
}
value = L.data[pos];
L.data[pos] = L.data[L.length - 1];
L.length--;
return true;
}
2 设计一个高效算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为O(1)
void Reverse(SqList &L){
// ElemType
int temp;
for(int i = 0; i < L.length / 2; i++){
temp = L.data[i];
L.data[i] = L.data[L.length - 1 - i];
L.data[L.length - 1 - i] = temp;
}
}
3 对长度为n的顺序表L,编写一个时间复杂度为O(n)、空间复杂度为O(1)的算法,该算法删除线性表中所有值为x的数据元素。
解法一:用step记录顺序表中等于x的个数,边扫描边统计step,并将不等于x的元素前移k个位置。扫描结束后修改L的长度
void deleteX(SqList &L,int e){
int step = 0;
for(int i = 0; i < L.length; i++){
if(L.data[i] == e)
step++;
else
L.data[i - step] = L.data[i];
}
L.length = L.length - step;
}
解法二:用k记录顺序表L中不等于x的元素个数(即需要保存的元素个数),扫描时将不等于x的元素移动到下标k的位置,并更新k值。扫描结束后修改L的长度。
void deleteX(SqList &L,int e){
int k = 0;
for(int i = 0; i < L.length; i++)
if(L.data[i] != x){
L.data[k] = L.data[i];
k++;
}
L.length = k;
}
4 从有序线性表中删除其值在给定s与t之间(要求s<t)的所有元素,若s或t不合理或顺序表为空,则显示出错信息并退出运行。
bool deleteBetweenSAndT(SqList &L,int s,int t){
if(L.length == 0 || s >= t)
return false;
int i,j;
for(i = 0; i < L.length && L.data[i] < s; i++); // 大于等于s的第一个元素
if(i >= L.length)
return false; // 所有元素都小于s
for(j = i; j < L.length && L.data[j] <= t; j++); // 大于t的第一个元素
for(; j < L.length; i++, j++){
L.data[i] = L.data[j];
}
L.length = i;
return true;
}
5 从顺序表中删除其值在给定s与t之间,(包括s和t,要求s<t)的所有元素,若s或t不合理或顺序表为空,则显示出错信息并退出运行。
bool deleteBetweenSAndT(SqList &L,int s,int t){
if(L.length == 0 || s >= t) return false;
int step = 0;
for(int i = 0; i < L.length; i++){
if(L.data[i] >= s && L.data[i] <= t)
step++;
else
L.data[i - step] = L.data[i];
}
L.length = L.length - step;
return true;
}
3,4,5可使用同一个。
6 从有序顺序表中删除所有值重复的元素,使表中所有元素的值均不同
解法一:双指针法
bool Delete_Same(SqList &L){
if(L.length == 0)
return false;
int i,j; // i存储第一个不相同的元素,j为工作指针
for(int i = 0; j = 1; j < L.length; j++)
if(L.data[i] != L.data[j])
L.data[++i] = L.data[j]; // i是第一个不相同的元素,要保留,所以要 ++
L.length = i + 1;
return true;
}
解法二:单指针法(个人写的)
void Delete_Same(SqList &L){
if(L.length <= 1) return ;
int step = 0;
for(int i = 1; i < L.length; i++){
if(L.data[i] == L.data[i - 1])
step++;
else
L.data[i - step] = L.data[i];
}
L.length = L.length - step;
}
7 将两个有序顺序表合并为一个新的有序顺序表,并由函数返回结果顺序表。
bool mergeList(SqList A,SqList B,SqList &C){
if(A.length + B.length > C.MaxSize)
return false;
int i = 0,j = 0,k = 0;
while(i < A.length && j < B.length){
if(A.data[i] < B.data[j])
C.data[k++] = A.data[i++];
else
C.data[k++] = B.data[j++];
}
while(i < A.length){
C.data[k++] = A.data[i++];
}
while(j < B.length){
C.data[k++] = B.data[j++];
}
C.length = k;
return true;
}
8 已知在一维数组A[m+n]中依次存放两个线性表(a1,a2,a3,…am)和(b1,b2,b3,…,bn)。编写一个函数,将数组中两个顺序表的位置互换,即将(b1,b2,b3,…,bn)放在(a1,a2,a3,…,am)的前面
三次翻转
void Reverse(int A[], int left, int right){
int temp;
for(int i = left; i <= (right + left) / 2; i++){
temp = A[i];
A[i] = A[right + left - i];
A[right + left - i] = temp;
}
}
void Exchange(int A[], int m, int n){
Reverse(A, 0, m + n - 1);
Reverse(A, 0, n - 1);
Reverse(A, n, m + n - 1);
}
9 线性表(a1,a2,a3,…,an)中的元素递增有序且按顺序存储于计算机内。要求设计一个算法,完成用最少时间在表中查找数值为x的元素,若找到,则将其与后继元素位置交换,若找不到,则将其插入表中并使表中元素仍递增有序。
折半查找 找小于等于x的最大元素。
void SearcjExchangeInsert(SqList &L, int x){
int l = 0;
int r = L.length - 1;
int mid;
while(l <= r){
mid = (l + r) / 2;
if(L.data[mid] == x)
break;
else if(L.data[mid] > x){
r = mid - 1;
}else{
l = mid + 1;
}
}
int t;
// 最后一个元素没有后继
if(L.data[mid] == x && mid != L.length - 1){
t = L.data[mid]; L.data[mid] = L.data[mid + 1]; L.data[mid + 1] = t;
}
int i;
if(l > r){
// r指向小于x的最大值
for(i = L.length - 1;i > r;i--)
L.data[i + 1] = L.data[i];
// r右边的值加入x
L.data[i + 1] = x;
L.length++;
}
}
链表
单链表
定义
使用LinkList(等价于LNode* )代指链表,见名知意
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
// 等价于
// typedef struct LNode --> LNode
// typedef struct LNode* --> LinkList
初始化
不含头结点
bool InitList(LinkList &L){
L = NULL;
return true;
}
含头结点
bool InitList(LinkList &L){
L = (LNode*) malloc(sizeof(LNode));
if(L == NULL)
return false;
L->next = NULL;
return true;
}
🐯 注意
-
含头结点的链表操作只有在初始化和销毁的时候使用
&
其他时候都不需要
因为在初始化或销毁的时候会对链表的头指针进行修改,而其他时候头指针都不会改变,都是通过指针来修改的内容。LinkList L; InitList(L); // 传的引用会影响返回后头指针L的值,所以不含头结点的插入、删除操作可能会对头指针进行修改
-
注意,这里是只针对含头结点的链表,和顺序表中的不同哦,顺序表的操作可不是指针操作
-
而不含头结点要根据情况决定,因为插入、删除操作可能会修改头结点
建立单链表 --> 头插法(常用于链表的逆置)
该函数可以多次插入,为空时可以正常插入,不为空时也可以正常插入
此处结束标志是输入 -1,可以自定义
不含头结点
LinkList List_HeadInsert(LinkList &L){
int x;
LNode* s;
scanf("%d",&x);
while(x != -1){
s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
break;
s->data = x;
s->next = L;
L = s;
scanf("%d",&x);
}
return L;
}
含头结点
LinkList List_HeadInsert(LinkList L){
// 如果L为空,要求L已经初始化,即L->next = NULL
// 如果L不为空,则向链表中继续加入
int x;
LNode* s;
scanf("%d",&x);
while(x != -1){ // 结束标志为 -1
s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
break;
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d",&x);
}
return L;
}
建立单链表 --> 尾插法
不含头结点
LinkList List_TailInsert(LinkList &L){
int x;
LNode* s;
LNode* r = L;
// 为空表时 r为NULL
while(r != NULL && r->next != NULL)
r = r->next;
scanf("%d",&x);
while(x != -1){
s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
break;
s->data = x;
if(r == NULL)
L = r = s;
else{
r->next = s;
r = s;
}
scanf("%d",&x);
}
r->next = NULL;
return L;
}
含头结点
LinkList List_TailInsert(LinkList L){
int x;
LNode* s;
// r为标尾指针 --> 课本上写法是L还未初始化,为L申请一个空间,并让r和L都指向该空间
// 由于我的参数是已经初始化的L,所以我要去找表尾
LNode* r = L;
while(r->next != NULL)
r = r->next;
scanf("%d",&x);
while(x != -1){
s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
break;
s->data = x;
r->next = s;
// 或者写 r = r->next 是一样的
r = s;
scanf("%d",&x);
}
// 表尾元素next设置为NULL
r->next = NULL;
return L;
}
插入 --> 按位序插入
不含头结点
bool ListInsert(LinkList &L,int i, int e) {
if(i < 1)
return false;
// i == 1 时需要修改头指针
if(i == 1){
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = e;
s->next = L;
L = s;
return true;
}
// 位序为1,指向L
LNode *p = L;
int j = 1;
while(p != NULL && j < i - 1){
p = p->next;
j++;
}
if(p == NULL)
return false;
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
含头结点
bool ListInsert(LinkList L,int i, int e) {
if(i < 1)
return false;
LNode *p = L;
int j = 0;
while(p != NULL && j < i - 1){
p = p->next;
j++;
}
if(p == NULL)
return false;
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
🐯 注意
初始编码时写为 LNode *p = L->next; int j = 1;
是错误的
如果初始链表为空,且要在第一个位置插入,是插入不成功的
插入 --> 在指定结点进行后插
不含头结点
和含头结点的一样,因为给定了一个结点都可以在其后面进行插入,所以插入的位置最小位序也是2
所以不会受头结点影响,不需要特殊考虑
含头结点
bool InsertNextNode(LNode *p, int e) {
// 加入 p == NULL 判断的好处
// 在其他函数调用时可以提高健壮性,如我查询值为x的结点
// 由于不存在返回NULL,而我没有进行判断,而是直接调用了该函数,但也不会出错
if(p == NULL)
return false;
LNode *s = (LNode* ) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
插入 --> 在指定结点进行前插
思路1:找到前驱,从前驱下手,时间复杂度为O(n)
思路2:仍进行后插,再通过交换数据来实现前插效果,时间复杂度为O(1)
法一:
不含头结点
bool InsertPriorNode(LinkList &L, LNode *p, int e){
if(p == NULL)
return false;
// 头结点特殊处理,会修改L,所以需要L传入引用
if(L == p){
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = e;
s->next = L;
L = s;
return true;
}
LNode *n = L;
while(n != NULL && n->next != p)
n = n->next;
if(n == NULL)
return false;
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = e;
s->next = n->next;
n->next = s;
return true;
}
含头结点
bool InsertPriorNode(LinkList L, LNode *p, int e){
// 头结点前不能插入
if(p == NULL || p == L)
return false;
// L的NULL在下方判断了
LNode *n = L;
while(n != NULL && n->next != p)
n = n->next;
// 等价写法
// while(n != NULL){
// if(n->next == p)
// break;
// n = n->next;
// }
if(n == NULL)
return false;
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = e;
s->next = n->next;
n->next = s;
return true;
}
法二:
不含头结点
由于本质还是后插,所以仍和前面的后插一样,含不含头结点代码都一样
值得一提的是,虽然实现的是前插,但是本处不需要修改头指针,因为数据元素位置不变,只是数据域变啦
含头结点
bool InsertPriorNode(LNode *p, int e){
if(p == NULL)
return false;
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->next = p->next;
p->next = s;
s->data = p->data;
p->data = e;
return true;
}
删除 --> 按位序删除
不含头结点
bool ListDelete(LinkList &L, int i, int &e){
if(i < 1 || L == NULL)
return false;
LNode *p = L;
int j = 1;
if(i == 1){
e = L->data;
L = L->next;
free(p);
return true;
}
while(p != NULL && j < i - 1){
p = p->next;
j++;
}
if(p == NULL || p->next == NULL)
return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
含头结点
bool ListDelete(LinkList L, int i, int &e){
if(i < 1)
return false;
LNode *p = L;
int j = 0;
while(p != NULL && j < i - 1){
p = p->next;
j++;
}
if(p == NULL || p->next == NULL)
return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
🐯 感悟
- 当函数操作会使用到某个结点的前驱时,含头结点的优势就会发挥出来,不需要单独考虑第一个数据元素。
删除 --> 指定结点的删除
思路1:找到前驱,修改前驱的后继指针,释放该结点,时间复杂度O(n)
思路2:利用该元素的后继,交换结点的数据域,然后删除后继结点,时间复杂度O(1)
当没有后继结点时,即该结点就是最后一个,还是需要去找前驱,时间复杂度O(n)。
不含头结点
// 思路1
bool DeleteNode(LinkList &L,LNode *p){
if(p == NULL)
return false;
if(L == p){
L = L->next;
free(p);
return true;
}
LNode *q = L;
while(q != NULL && q->next != p){
q = q->next;
}
if(q == NULL)
return false;
q->next = p->next;
free(p);
return true;
}
// 思路2
// 该函数也不会修改头指针
bool DeleteNode(LNode *p){
LNode *q = p->next;
// 这里应有判断 q 如果等于 NULL 应该如何做!按理来说不能返回false,因为实际上是可以删除的
// 做法1:传参时传入LinkList &L,如果 p->next == NULL ,就去找前驱删除
// 做法2: 在第一个函数中加入判断,当p->next != NULL时,调用第二个函数,即该函数,实现函数复用,减少代码量
// 做法3:在用户使用时加入判断。
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
含头结点
// 思路1
bool DeleteNode(LinkList &L,LNode *p){
if(p == NULL)
return false;
LNode *q = L;
while(q != NULL && q->next != p){
q = q->next;
}
if(q == NULL)
return false;
q->next = p->next;
free(p);
return true;
}
// 思路2
// p处于尾结点的处理方式同上
bool DeleteNode(LNode *p){
LNode *q = p->next;
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
查找 --> 按位查找
不含头结点
LNode *GetElem(LinkList L,int i){
if(i < 1)
return NULL;
LNode *p = L;
int j = 1;
while(p != NULL && j < i){
p = p->next;
j++;
}
return p;
}
含头结点
i == 0
时返回头结点
LNode *GetElem(LinkList L, int i){
// 此处加不加该判断都不影响最终结果,因为下面的 j < i 也会进行判断
if(i < 0)
return NULL;
int j = 0;
LNode *p = L;
while(p != NULL && j < i){
p = p->next;
j++;
}
return p;
}
查找 --> 按值查找
不含头结点
LNode *LocateElem(LinkList L,int e){
LNode *p = L;
while(p != NULL && e != p->data){
p = p->next;
}
return p;
}
含头结点
LNode *LocateElem(LinkList L,int e){
LNode *p = L->next;
while(p != NULL && e != p->data){
p = p->next;
}
return p;
}
🐯 提示
- 在上述操作中,很多地方都可以实现函数的复用,实际开发中也应当如此。
- 函数传入的参数
LinkList L
都应该是初始化过的。
求表长
不含头结点
int length(LinkList L){
LNode *p = L;
int len = 0;
while(p != NULL){
len++;
p = p->next;
}
return len;
}
含头结点
int length(LinkList L){
LNode *p = L->next;
int len = 0;
while(p != NULL){
len++;
p = p->next;
}
return len;
}
判空
不含头结点
bool Empty(LinkList L){
return L == NULL;
}
含头结点
bool Empty(LinkList L){
return L->next == NULL;
}
双链表
定义
typedef struct DNode{
int data;
struct DNode *prior, *next;
}DNode, *DLinkList;
初始化
不含头结点
bool InitDLinkList(DLinkList &L){
L = NULL;
return true;
}
含头结点
bool InitDLinkList(DLinkList &L){
L = (DNode*) malloc(sizeof(DNode));
if(L == NULL)
return false;
L->prior = NULL;
L->next = NULL;
return true;
}
判空
不含头结点
bool Empty(LinkList L){
return L == NULL;
}
含头结点
bool Empty(LinkList L){
return L->next == NULL;
}
插入 --> 在某结点后插
在某结点前插 时间复杂度和单链表相比就变为了O(1)
含头结点
// p 后插 s
bool InsertNextNode(DNode *p, DNode *s){
if(p == NULL || s == NULL)
return false;
s->next = p->next; // 1
if(p->next != NULL)
p->next->prior = s; // 2
s->prior = p; // 3
p->next = s; // 4
}
🐯 思考
- 第3个语句可以在任意位置,因为s的前项是确定的,为p,且不会被修改
- 第1个语句需要在4语句之前,因为1需要访问
p->next
,所以要在它被修改前完成 - 第2个语句也要在4之前,因为2语句会访问到
p->next
,所以要在它被修改前完成
观察赋值表达式左右两侧, 1 (左侧s->next 是被修改的,不会被用到,而右侧p->next会被修改,所以要在被修改之前,即在4语句之前) 2 (左侧p->next会被修改,所以要在被修改之前,即在4语句之前,而右侧s相当于常量,不会被改动,所以不影响) 3 (s->prior 是被修改的,其他语句不会用到,且p是固定的,也不会被修改,所以放到任意位置都可以) 4 (p->next被修改,所以要在1,2之后)
综上,只要满足 1,2 在 4 之前,则任何语序都可以
遍历
// 后向遍历
while(p != NULL){
p = p->next;
}
// 前向遍历 (适用于不含头结点的链表)
while(p != NULL){
p = p->prior;
}
// 前向遍历,但此处不会遍历头结点
while(p->prior != NULL){
p = p->prior;
}
查找 --> 按值查找与按位查找都和单链表一样,不需要修改
删除 --> 删除某个结点后继
bool DeleteNextNode(DNode *p){
if(p == NULL || p->next == NULL)
return false; //前者是传入错误,后者是该结点没有后继
DNode *q = p->next;
if(q->next != NULL)
q->next->prior = p;
p->next = q->next;
free(p);
return true;
}
🐯 注意
- 双链表和单链表相比,插入、删除操作都很方便,但是相比较而言,双链表操作两个指针,更复杂(王道书有云)
- 按位删除、插入、在某结点后插,时间复杂度都一样,但是删除某结点、实现某结点前插而言,双链表更稳一些,单链表虽然有骚套路实现O(1)时间复杂度,但是在删除尾结点时,单链表还是O(n)复杂度。
销毁
void DestoryList(DLinkList &L){
while(L->next != NULL)
DeleteNextDNode(L);
free(L);
L = NULL; // 头指针指向NULL
}
🐯 注意
- 清空链表:将所有除头节点以外的存放有数据的节点释放掉
- 销毁链表:将包括头结点在内的所有节点释放掉
- 清空后链表还能用,销毁后链表就不能用了。
循环链表
循环单链表
定义
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
初始化
bool InitList(LinkList &L){
L = (LNode*) malloc(sizeof(LNode));
if(L == NULL)
return false;
L->next = L;
return true;
}
判空
bool Empty(LinkList L){
return L->next == L;
}
判断是不是表尾结点
bool isTail(LinkList L,LNode *p){
return p->next == L;
}
插入与删除 同单链表几乎一样,实际上还会更简单,因为循环单链表是一个环,所有指针域都没有NULL,不需要向单链表那样考虑操作的结点是不是尾结点
🐯 思考
- 在单链表中设置的头指针,在表头插入元素时间复杂度为
O(1)
,在表尾插入元素时间复杂度为O(n)
- 在循环单链表中设置的头指针,在表头插入元素时间复杂度为
O(1)
,在表尾插入元素时间复杂度为O(n)
- 在循环单链表中设置的尾指针,在表头插入元素时间复杂度为
O(1)
,在表尾插入元素时间复杂度为O(1)
- 为什么没有讨论单链表中设置尾指针? --> 不要头指针,只要尾指针,那势必要
p->prior
不要p->next
,如此才能通过尾指针方位所有结点,等价于单链表设置了头指针从前往后扫描。
循环双链表
定义
typedef struct DNode{
int data;
struct DNode *prior,*next;
}DNode, *DLinkList;
初始化
bool InitList(DLinkList &L){
L = (DNode*) malloc(sizeof(DNode));
if(L == NULL)
return false;
L->next = L;
L->prior = L;
return true;
}
判空与判断是否为表尾结点和循环单链表一样
插入和删除和双链表相比,在对某个结点进行操作时(非头头结点)不需要判断后继或者前驱是否为NULL,直接操作即可
静态链表
定义
typedef struct{ //
int data;
int next;
}SLinkList[MaxSize];
// SlinkList s; ==> s为一个大小为MaxSize的数组
等价于如下
struct Node{
int data;
int next;
};
typedef struct Node SLinkList[MaxSize]; // SLinkList是一个Node型数组
👀 吐槽
- 好鸡肋啊,在静态数组中设立链表。(ps: 好像是用于某些不支持指针的高级语言但又想使用链表的的情况中)
初始化
// 王道课中讲授的方法,-1代表尾结点,-2代表空闲
void InitList(SLinkList s){
s[0].next = -1;
for(int i = 1; i < MaxSize; i++){
s[i].next = -2;
}
}
// 课本讲授的,感觉更好一些
// 因为上面的方法,如果要加入一个结点,首先要找到表尾结点,还要找空闲结点,对两个进行修改
int avil;
void InitList(SLinkList s){
s[0].next = -1;
avil = 1; // 当前可用结点的游标
for(int i = 1; i < MaxSize - 1; i++)
s[i].next = i + 1; // 都指向下一个位置,构成空闲链表链
s[MaxSize - 1].next = -1;
// 初始化结束后相当于有两个链表,一个空闲链表 一个工作链表
}
插入 --> 表尾插入
// 第二种初始化方式,实现表尾插入
bool Append(SLinkList s,int x){
if(avil == -1)
return false; // 没有空闲空间
// 通过avil,将元素存入该位置,并设置并修改avil的值
int q = avil;
s[avil].data = x;
avil = s[avil].next;
s[q].next = -1;
// 从头结点开始找到表尾,将该元素放入表尾
int p = 0;
while(s[p].next != -1)
p = s[p].next;
s[p].next = q;
return true;
}
// 第一种就需要遍历,从头结点开始找到next值为-1的点即尾结点
// 再从头对数组遍历(而不是通过next指向去找),找值为-2的空闲结点,然后操作,这里感觉并不好,有点违背链表的定义形式,直接遍历数组,,,
插入 --> 按位序插入
bool Insert(int i, int x){
1. 从上到下扫描或从下到上扫描,找到一个空结点,存入数据元素
2. 从头结点出发找打位序为 i-1 的结点
3. 修改新节点的 next 指向 i-1 结点的 next
4. 修改 i-1 结点的 next 指向新结点的next
}
删除
bool Remove(int i){
1. 从头结点开始找到前驱结点
2. 修改前驱结点的next
3. 被删除结点next设置为-2,即空闲状态
}
🐯 提示
- 插入、删除、查找都和动态链表一样,只不过分配空间变为查找空闲结点,释放空间变为修改值变为
-2
算法设计
1 设计一个递归算法,删除不带头结点的单链表L中所有值为x的结点。
思路:
终止条件:f(L,x) = 不做任何事, L为空
递归主体:f(L,x) = 删除L结点;f(L->next,x);
若 L->next == x
f(L,x) = f(L->next,x); 其他情况
void DelXNode(LinkList &L,int x){
if(L == NULL)
return ;
LNode *p;
if(L->data == x){
p = L;
L = L->next;
free(p);
DeleteX(L,x);
}else{
DeleteX(L->next,x); // 这里传入L->next,如果在下一个结点被删除(L->next->next),那么L->next也会被修改为L->next->next;
}
}
void DelXNode(LinkList &L, int x){
if(L == NULL)
return ;
DelXNode(L->next, x); // 如果下一个结点被删除,会实现L->next = L->next->next; 且下一个结点被释放
if(L->data == x){
LNode *p = L;
L = L->next;
free(p);
}
}
🐯 此处重点为理解递归引用的作用。
2 在带头结点的单链表L中,删除所有值为x的结点,并释放其空间,假设值为x的结点不唯一,试编写算法以实现上述操作。
法一:
// 递归算法 --> 传入的是L,但是在函数中判断 L->next 是否需要删除,如此解决了头结点问题,也可以很好的修改前驱指针。
void DelXNode(LinkList L,int x){
if(L->next == NULL) return;
LNode *p;
p = L->next;
if(p->data == x){
L->next = p->next;
free(p);
DeleteX(L,x);
}else{
DeleteX(p,x);
}
}
法二:
// 遍历删除,和法一思路一样,实现方式不同
void DelXNode(LinkList L, int x){
LNode *p = L->next, *pre = L, *q;
while(p != NULL){
if(p->data == x){
q = p;
pre->next = p->next;
p = p->next;
free(q);
}else{
pre = p;
p = p->next;
}
}
}
法三:
// 后插法实现 --> 遍历所有结点,如果该结点数据域为x,则删除,否则加入表尾。
// 此处r指向L,p指向一个链表,实际上,从第一个等于x的结点被删除后,就会产生两个链表,一个是头结点
// 指针L,尾结点指针为r的我们需要的链表,另一个是不含头指针,第一个结点指针为p的链表。
void DelXNode(LinkList L, int x){
LNode *r = L, *p = L->next, *q;
while(p != NULL){
if(p->data != x){
r->next = p;
r = p;
p = p->next;
}else{
q = p;
p = p->next;
free(q);
}
}
r->next = NULL;
}
3 设L为带头结点的单链表,编写算法实现从尾到头反向输出每个结点的值。
思路:
-
将链表逆置后输出,有点繁琐
1. 逆序输出第一反应是栈的性质,此处也可以借助栈实现 2. 借助递归实现逆序输出
// 留意尾结点
// 每次都是在打印下一个结点的值,当下一个结点为NULL时,也就不打印了。
void Reverse_Print(LinkList L){
LNode *p = L->next;
if(p != NULL){
Print_Reverse(p);
printf("%d ",p->data);
}
}
// 王道书有云(没我的好,哈哈哈哈~)
// 王道思路是单独编写一个函数跳过头结点,每次打印该结点的值
void R_Print(LinkList L){
if(L->next != NULL)
R_Print(L->next);
if(L != NULL) print("%d ",L->data);
}
void R_Ignore_Head(LinkList L){
if(L->next != NULL) R_Print(L->next);
}
4 试编写在带头结点的单链表L中删除一个最小值结点的高效算法(假设最小值结点是唯一的)。
// 也是遍历链表,通过一个min来记录最小值,minpre记录最小值结点的前驱
// 缺点:仅适用于数据域为int型的 --> 考试不要出现这种!!!谨记
void Delete_Min(LinkList L){
if(L->next == NULL) return; // 保证至少有一个元素
LNode *p = L->next, *minpre = L; // minpre存储最小值结点的前驱
int min = p->data;
while(p->next != NULL){
if(p->next->data < min){
minpre = p;
min = p->next->data;
}
p = p->next;
}
// 删除 q 的后继
LNode *s = minpre->next;
minpre->next = s->next;
free(s);
}
// 王道书有云
// 利用四个指针,分别指向当前结点,当前结点的前驱,最小值结点,最小值结点的前驱,代码更易懂
// 弥补了上面代码仅适用于数据型为int型的缺点
void Delete_Min(LinkList L){
LNode *p = L->next, *minp = p; // p,pre为工作指针
LNofr *pre = L, *minpre = pre;
while(p != NULL){
if(p->data < minp->data){
minpre = pre;
minp = p;
}
}
minpre->next = minp->next;
free(minp);
}
5 试编写算法将带头结点的单链表就地逆置,所谓“就地”是指辅助空间的空间复杂度为O(1)
法一:更易理解
// 头插法实现逆置
// 将头结点摘下,相当于一个空链表和一个不带头结点的链表。
// 从第一个结点开始依次插入。--> 和题2类似
void Reverse(LinkList L){
LNode *p = L->next, *r;
L->next = NULL; // 第一个结点会移动到最后,所以要在初始时赋值。让第一个结点来头插时,next被赋值为NULL
while(p != NULL){
r = p;
p = p->next;
r->next = L->next;
L->next = r;
}
}
法二:
void Reverse(LinkList L){
if(L->next == NULL) return ;
// 头结点 pre p r 将元素的指针域修改来指向前驱结点
// 第一个结点的指针域设置为NULL,头结点在最后指向尾结点
LNode *pre, *p = L->next, *r = p->next;
p->next = NULL; // 第一个结点的NULL置为空
while(r != NULL){
pre = p;
p = r;
r = r->next;
p->next = pre;
}
// r == NULL 时,p在最后一个结点
L->next = p;
}
6 有一个带头结点的单链表L,设计一个算法使其元素递增有序。
法一:
思路:将链表拆开,分成一个只含头结点的空链表和剩余结点构成的链表,依次往里插。
void Sort(LinkList L){
LNode *p = L->next, *q = L, *r;
q->next = NULL;
while(p != NULL){
q = L;
// 如果q->next == NULL 那么说明到了末尾,q即指向末尾
// 如果q->next->data > p->data 说明 q->data < p->data,插入到q后面即可。--> 很重要,用q->next去比较!
while(q->next != NULL && q->next->data < p->data){
q = q->next;
}
r = p;
p = p->next;
r->next = q->next;
q->next = r;
}
}
思路:(王道做法)将链表拆开,分成一个只含头结点和一个数据元素的链表和剩余结点构成的链表,依次往里插。
// 王道书的做法,和上面的思路是一样的,实现略微有差异,且书上对于只有一个结点的链表排序会报错,所以省略...
法二:将链表的内容先拷贝到数组,排序后再依次插入到链表中,时间复杂度可达到 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
7 设在一个带表头结点的单链表中所有元素结点的数据值无序,试编写一个函数,删除表中所有介于给定的两个值(作为函数参数给出)之间的元素(若存在)。
// 同题2,只是判断条件不同而已,也可以写递归、遍历在原链表删除,也可以使用后插法实现。此处采用后插法
void RangeDelete(LinkList L, int min, int max) {
LNode *p = L->next, *q, *r = L;
while(p != NULL){
q = p;
p = p->next;
if(q->data >= min && q->data <= max)
free(q);
else{
r->next = q;
r = q;
}
}
r->next = NULL;
}
8 给定两个单链表,编写算法找出两个链表的公共结点
思路:当有公共结点时,其后所有的结点都相同,所以当两个链表长度不相同时,公共结点一定不在长度较长链表的前 (longLen - shortLen) 个中。如下图所示,一定不在1中,而在2中。
LNode *FindPublic(LinkList L1, LinkList L2){
int len1 = 0, len2 = 0;
LNode *p = L1->next, *q = L2->next;
// 求表长
while(p != NULL){
len1++;
p = p->next;
}
while(q != NULL){
len2++;
q = q->next;
}
// p 为长的那一个,q为短的那一个
int dist;
if(len1 > len2){
p = L1->next;
q = L2->next;
dist = len1 - len2;
}else{
p = L2->next;
q = L1->next;
dist = len2 - len1;
}
while(dis--) p = p->next;
while(p != NULL && p != q){ // 未到表尾
p = p->next;
q = q->next;
}
// 相等时则找到公共结点,不相等则是NULL
return p;
}
🐯 思考
- 该算法的时间复杂度为 O ( l e n 1 + l e n 2 ) O(len1 + len2) O(len1+len2),如果要两层循环遍历比较,时间复杂度为 O ( l e n 1 ∗ l e n 2 ) O(len1 * len2) O(len1∗len2)
- 如何判断有公共结点? --> 分别找到两个尾结点,如果相同,那么必有公共结点,只不过位置不知道。如果是双链表,那么我们就可以从尾结点出发,向前找,知道都往前走K步,两个结点指针不同时,则公共长度为K。但是此处不是双链表,但是可以发现,公共部分一定是在末尾,且长度相同,利用该规律得该题解法。
9 给定一个带表头结点的单链表,设置head为头指针,结点结构为(data,next),data为整型元素,next为指针,试写出算法:按递增次序输出单链表中各结点的数据元素,并释放结点所占的存储空间(要求:不允许使用数组作为辅助空间)。
法一:排序后输出,做法同第6题
法二:(王道书做法)每次输出最小元素并删除,时间复杂度也是 O ( n 2 ) O(n^2) O(n2)
void Sort_Print(LinkList head){
LNode *pre; // 最小结点的前驱
LNode *p; // 工作指针
LNode *u;
while(head->next != NULL){
p = head->next;
pre = head;
while(p->next != NULL){
if(p->next->data < pre->next->data) // 比较
pre = p;
p = p->next;
}
cout << pre->next->data << " ";
u = pre->next;
pre->next = u->next;
free(u);
}
free(head);
}
10 将一个带头结点的单链表A分解为两个带头结点的单链表A和B,使得A表中含有原表中序号为奇数的元素,而B表中含有原表中序号为偶数的元素,且保持其相对顺序不变。
思路:在A链表上,设置计数器,当为偶数结点时,后插到B链表。
LinkList Split(LinkList A){
LinkList B = (LinkList) malloc(sizeof(LNode));
B->next = NULL;
LNode *p = A->next;
LNode *r = B;
LNode *pre = A;
int i = 1;
while(p != NULL){
if(i % 2 == 0){
pre->next = p->next;
r->next = p;
r = p;
}else
pre = p; // 在两种情况下都要进行 p = p->next;
p = p->next;
i++;
}
r->next = NULL;
return B;
}
🐯 思考
- 可不可以不设置计数器?
可以的,因为只有两种操作交替进行,所以我可以在循环中 1. 指针后移 2. 结点插入B,如此循环,相当于每次做两步操作,不过要注意 1,2之间要判断p ? NULL
思路二:(王道有云) 将 p = A->next; A->next = NULL;
,A表置空,然后p作为工作指针去遍历,为奇插入A,为偶插入B。(设置两个尾指针用于后插,一个为A的尾指针,一个为B的尾指针)。
11 设C = { a 1 , b 1 , a 2 , b 2 , . . . , a n , b n } \left\{ a_1,b_1,a_2,b_2,...,a_n,b_n \right\} {a1,b1,a2,b2,...,an,bn} 为线性表,采用带头结点的单链表存放,设计一个就地算法,将其拆分为两个线性表,使得 A = { a 1 , a 2 , . . . , a n } A = \left\{a_1,a_2,...,a_n\right\} A={a1,a2,...,an} , B = { b n , . . . , b 2 , b 1 } B=\left\{b_n,...,b_2,b_1\right\} B={bn,...,b2,b1} 。
void SplitC(LinkList C, LinkList A, LinkList B){
LNode *p = C->next;
C->next = NULL;
LNode *r = A, *q;
while(p != NULL){
r->next = p;
r = p;
p = p->next;
// 这里其实不需要判断 p ? NULL,因为题目给的是 2*n个结点。 王道书给加上了
q = p;
p = p->next;
q->next = B->next;
B->next = q;
}
r->next = NULL;
}
12 在一个递增有序的线性表中,有数值相同的元素存在。若存储方式为单链表,设计算法去掉数值相同的元素,使表中不再有重复的元素,例如 ( 7 , 10 , 10 , 21 , 30 , 42 , 42 , 42 , 51 , 70 ) (7,10,10,21,30,42,42,42,51,70) (7,10,10,21,30,42,42,42,51,70) --> ( 7 , 10 , 21 , 30 , 42 , 51 , 70 ) (7,10,21,30,42,51,70) (7,10,21,30,42,51,70) 。
思路一:当前指针指向的结点与后一个结点相比,如果相同则删除后一个结点,如果不相同则指针后移。
void Remove_Same(LinkList L){
if(L->next == NULL) return;
LNode *q = L->next, *u;
LNode *p = q->next;
while(p != NULL){
if(p->data == q->data){
q->next = p->next;
u = p;
p = p->next;
free(u);
}else{
q = p;
p = p->next;
}
}
}
// 王道书有云 (代码更简洁)
void Remove_Same(LinkList L){
if(L->next == NULL) return;
LNode *p = L->next, *q;
while(p != NULL){
q = p->next;
if(p->data == q->data){
p->next = q->next;
free(q);
}else
p = p->next;
}
}
思路二:后插法,将结点按顺序插入链表,如果工作结点与尾结点值相同,则释放,反之插入。
void Remove_Same(LinkList L){
LNode *p = L->next, *r = L, *q;
L->next = NULL;
while(p != NULL){
if(r == L || p->data != r->data){
r->next = p;
r = p;
p = p->next;
}else{
q = p;
p = p->next;
free(q);
}
}
r->next = NULL;
}
13 假设有两个按元素值递增次序排列的线性表,均以单链表形式存储,请编写算法将这两个单链表归并为一个按元素值递减次序排列的单链表,并要求利用原来两个单链表的结点存放归并后的单链表。
思路:两个链表按照首元素逐一比较,小的先插入,使用头插法,最后可实现逆序。
void MergeList(LinkList A, LinkList &B){
LNode *p = A->next, *q = B->next, *t;
A->next = NULL;
B->next = NULL;
while(p && q){ // p != NULL && q != NULL
if(p->data < q->data){
// 头插法
t = p;
p = p->next;
}else{
t = q;
q = q->next;
}
t->next = A->next;
A->next = t;
}
if(p)
q = p; // 因为如果有,一定有且仅有一个是非空的 ,统一操作
while(q != NULL){
t = q;
q = q->next;
t->next = A->next;
A->next = t;
}
free(B); // 释放掉B,所以B要传引用
}
14 设A和B是两个单链表(带头结点),其中元素递增有序。设计一个算法从A和B中的公共元素产生单链表C,要求不破换A、B的结点。
思路:A、B分别设置一个工作指针,并比较数据域大小,如果相等则创建结点并插入,如果不相等,较小的那个指针后移。当任一链表遍历完毕,则说明不会再有公共元素,循环结束。
LinkList Get_Common(LinkList A, LinkList B){
LinkList C = (LinkList) malloc(sizeof(LNode));
C->next = NULL;
LNode *p = A->next, *q = B->next, *r = C, *s;
while(p && q){
if(p->data == q->data){
s = (LNode*) malloc(sizeof(LNode));
s->data = p->data;
r->next = s;
r = s;
p = p->next;
q = q->next;
}else if(p->data < q->data)
p = p->next;
else
q = q->next;
}
r->next = NULL;
return C;
}
15 已知两个链表A和B分别表示两个集合,其元素递增排列。编制函数,求A与B的交集,并存放于A链表中。
// 时间复杂度 O(len1 + len2)
void Union(LinkList A,LinkList &B){
LNode *p = A->next, *q = B->next, *r = A, *up,*uq;
A->next = NULL;
while(p && q){ // 相同的则保留一个
if(p->data == q->data){
r->next = p;
r = p;
uq = q;
p = p->next;
q = q->next;
free(uq);
}else if(p->data < q->data){
up = p;
p = p->next;
free(up);
}
else{
uq = q;
q = q->next;
free(uq);
}
}
while(q){ // 剩余链表空间全部释放
uq = q;
q = q->next;
free(uq);
}
while(p){
up = p;
p = p->next;
free(up);
}
r->next = NULL;
free(B);
}
16 两个整数序列 A = a 1 , a 2 , . . . , a m A = a_1,a_2,...,a_m A=a1,a2,...,am 和 B = b 1 , b 2 , . . . , b n B=b_1,b_2,...,b_n B=b1,b2,...,bn 已经存入两个单链表中,设计一个算法,判断序列B是否是序列A的连续子序列。
// 我的做法
// 只需比较 A 链表中前 len(A) - len(B) + 1 个作为起点的情况
bool Pattern(LinkList A, LinkList B){
LNode *p = A->next, *q = B->next, *bp, *bq;
int lenA = length(A), lenB = length(B), i = 0;
while(i < lenA - lenB + 1){
if(p->data == q->data){
bp = p;
bq = q;
while(bp && bq){
if(bp->data != bq->data) break;
bp = bp->next;
bq = bq->next;
}
if(bq == NULL) return true;
}
p = p->next;
i++;
}
return false;
}
// 王道书做法
// 有一部分无用功,即B的长度小于A时,必然不是子序列
int Pattern(LinkList A, LinkList B){
LNode *p = A, *q = B, *pre = p;
while(p && q){
if(p->data == q->data){
p = p->next;
q = q->next;
}else{
pre = pre->next;
p = pre; // p向后移动一个
q = B;
}
}
if(q == NULL) return 1;
else return 0;
}
// 王道与我做法的结合
// 不如前两个
bool Pattern(LinkList A, LinkList B){
LNode *p = A->next, *q = B->next, *pre = p;
int lenA = length(A), lenB = length(B), i = 0; // 只需比较 A 链表中前 len(A) - len(B) + 1 个作为起点的情况
while(i < lenA - lenB + 1){
if(p->data == q->data){
while(p && q){
if(p->data != q->data) break;
p = p->next;
q = q->next;
}
if(q == NULL) return true;
else{// 继续扫描
pre = pre->next;
p = pre;
q = B->next;
}
}else
pre = p = p->next;
i++;
}
return false;
}
17 设计一个算法用于判断带头结点的循环双链表是否对称。
bool Symmetry(DLinkList L){
LNode *p = L->next, *r = L->prior;
if(p == NULL) return false; // 空链表
// 奇数个 偶数个
while(p != r && p->next != r){
if(p->data != r->data) return false;
p = p->next;
r = r->prior;
}
return true;
}
🐯 注意
- 结点数量会影响判断条件的哦!
18 有两个循环单链表,链表头指针分别为h1和h2,编写一个函数将链表h2链接到链表h1之后,要求链接后的链表仍保持循环链表形式。
void Merge(LinkList A, LinkList B){
// 因为要修改尾结点指针域,所以要找到A,B链表的尾结点
LNode *p = A, *q = B;
while(p->next != A) p = p->next;
while(q->next != B) q = q->next;
p->next = B->next; // 王道书是让 p->next = B; 感觉并不合理
q->next = A;
free(B);
}
19 设有一个带头结点的循环单链表,其结点值均为正整数。设计一个算法,反复找出单链表中结点值最小的结点并输出,然后将该结点从中删除,知道单链表为空为止,再删除表头结点。
void Del_All(LinkList L){
LNode *p, *minpre, *u; // 王道书使用p,pre,min,minpre四个结点,实质上有两个是多余的,因为pre始终是p的前驱,minpre始终是min的前驱,所以用两个就可以。
while(L->next != L){
p = L->next; minpre = L;
while(p->next != L){
if(minpre->next->data > p->next->data)
minpre = p;
p = p->next;
}
cout << minpre->next->data << " ";
u = minpre->next;
minpre->next = minpre->next->next;
free(u);
}
free(L);
}
20 设头指针为L的带有表头结点的非循环双向链表,其每个结点中除有pre(前驱指针)、data(数据)和next(后继指针)域外,还有一个访问频度域freq。再链表被启动前,其值均初始化为零。每当在链表中进行一次
Locate(L,x)
运算时,令元素值为x的结点中的freq域的值增1,并使此链表中结点保持按访问频度非增(递减)的顺序排列,同时最近访问的结点排在频度相同的结点前面,以便使频繁访问的结点总是靠近表头。试编写符合上述要求的Locate(L,x)
运算的算法,该运算为函数过程,返回找到结点的地址,类型为指针型。
思路:访问后,A结点的freq加1,同时向前遍历,如果结点的freq值小于等于A结点freq的值,就继续向前,直到头结点或大于A结点的,插在其后。
DNode *Locate(DLinkList L, int x){
DNode *p = L->next, *q;
while(p && p->data != x)
p = p->next;
if(p == NULL) return NULL; // 为空表或找不到该值
p->freq++;
q = p->prior;
while(q != L && q->freq <= p->freq)
q = q->prior;
// 将p结点从链表中摘下
if(p->next != NULL)
p->next->prior = p->prior;
p->prior->next = p->next;
// 插入到目标结点q后面
p->next = q->next;
if(q->next != NULL) // 如果值为x的结点在末尾,且freq+1后位置不变,则此处如果不加上判断会出错
q->next->prior = p;
p->prior = q;
q->next = p;
return p;
}
21 单链表有环,是指单链表的最后一个结点的指针指向了链表中的某个结点(通常单链表的最后一个结点的指针域是空的)。试编写算法判断单链表是否存在环。
思路:设置一个slow,fast,fast每次走两步,slow每次走一步,如果没有环,由于fast走的快,那么fast或fast->next最终会为NULL。如果有环,那么fast和slow最终都会走进环,刚进入环,如果两者有一段距离,那么一定会慢慢接近,(fast走两步,slow走一步,每次二者距离-1,最终一定会相遇)。
具体思路,,,看王道书吧。。。好好琢磨琢磨
LNode* FindLoopStart(LNode *head){
LNode *fast = head, *slow = head;
while(fast != NULL && fast->next != NULL){
slow = slow->next;
fast = fast->next->next;
if(fast == slow) break; // 相遇
}
if(fast == NULL || fast->next == NULL) return NULL; // 没有环,返回NULL
LNode *p1 = head, *p2 = slow;
while(p1 != p2){
p1 = p1->next;
p2 = p2->next;
}
return p1; // 入口点
}
栈
顺序栈
结构体定义
#define MaxSize 10
typedef struct{
int data[MaxSize];
int top;
}SqStack;
常用操作
// 初始化栈
void InitStack(SqStack &S){
S.top = -1;
}
// 判断栈空
bool StackEmpty(SqStack S){
return S.top == -1;
}
// 进栈
bool Push(SqStack &S, int x){
if(S.top == MaxSize - 1)
return false;
S.data[++S.top] = x;
return true;
}
// 出栈
bool Pop(SqStack &S, int &x){
if(S.top == -1)
return false;
x = S.data[S.top--];
return true;
}
// 获取栈顶
bool GetTop(SqStack S, int &x){
if(S.top == -1)
return false;
x = S.data[S.top];
return true;
}
// 栈长
int Length(SqStack S){
return S.top + 1;
}
// 销毁
void DestroyStack(SqStack &S){
S.top = -1;
}
也可以使用 top = 0 进行初始化,如此那么以上所有操作都会进行略微改动
共享栈
结构体定义
#define MaxSize 10
typedef struct{
int data[MaxSize];
int top0;
int top1;
}ShStack;
初始化
void InitStack(ShStack &S){
S.top0 = -1;
S.top1 = MaxSize;
}
两个栈在使用过程中向中间逐渐靠近,共享同一片空间,提高内存资源利用率。
栈满条件 S.top0 + 1 == S.top1;
这是两个顺序栈共用一个存储空间而已,实际上还是两个栈,而不是一个栈,所以判空的时候要注意问的是哪一个栈为空
链式栈
结构体定义
typedef struct LinkNode{
int data;
struct LinkNode *next;
}*LiStack, LinkNode;
带头结点和不带头结点均可以实现,不过比较推荐使用不带头结点的,下面示例代码也使用不带头结点的
常见操作
void InitStack(LiStack &L){
L = NULL;
}
bool StackEmpty(LiStack L){
return L == NULL;
}
bool Push(LiStack &L, int x){
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false; // 空间不足
s->data = x;
if(L == NULL){ // 为空栈时
s->next = NULL;
L = s;
return true;
}
// 不为空栈时使用头插法
s->next = L;
L = s;
return true;
}
bool Pop(LiStack &L, int &x){
if(L == NULL)
return false;
x = L->data;
LNode *u = L;
L = L->next;
free(u);
return true;
}
bool GetTop(LiStack L, int &x){
if(L == NULL)
return false;
x = L->data;
return true;
}
int Length(LiStack L){
LNode *p = L;
int len = 0;
while(p){
len++;
p = p->next;
}
return len;
}
void DestroyStack(LiStack &L){
LNode *p = L, *s;
while(p){
s = p;
p = p->next;
free(s);
}
}
为什么这些操作都在表头进行操作?表尾不行吗?
也可以,不过定义相对麻烦,链式栈基于单链表实现,只有next指针,如果在表尾执行插入删除,需要一个表尾指针,这样才能在插入和删除时才能是O(1)复杂度,那么表尾指针定义在哪里比较何时呢?最好的方式是再定义一个结构体,里面存放 LiStack 和 *r,即一个链式栈和一个表尾指针。
算法设计
3 栈的初态和终态都为空,试设计一个算法,判定所给的操作序列是否合法,合法返回true,不合法返回false。I表示输入,O表示输出。
bool Judge(const char *s){
int top = -1, len = strlen(s);
for(int i = 0; i < len; i++){
if(s[i] == 'I') // 入栈不会引起上溢,不用判断
top++;
else{
if(top == -1) // 出栈前判断一下可否出栈
return false;
top--;
}
}
return top == -1;
}
王道书上的过于复杂化。
4 设单链表的表头指针为L,结点结构由data和next两个域构成,其中data域为字符型。试设计算法判断该链表的全部n个字符是否中心对称。例如xyx、xyyx都是中心对称。
思路:由于大小已知,所以遍历链表前半部分,将链表中的元素存储在栈中,再依次出栈并同时与链表后半部分进行比较,看是否相同。
bool dc(LinkList L,int n){
char s[n/2];
LNode *p = L->next;
int i = 0;
while(i < n / 2){
s[i++] = p->data;
p = p->next;
}
if(n % 2)
p = p->next;
//while(p != NULL){
// if(s[--i] != p->data) return false;
// p = p->next;
//}
//return true;
//上述五行代码可同步替换为
while(p != NULL && s[--i] == p->data) // 没有对i的范围进行判断,不必担心下标,因为元素个数是确定的
p = p->next;
return i == 0;
}
🐯 思考
- 如果未知大小,则需要先遍历得到大小。
- 如果未知大小,也可以使用链式栈,将所有元素存入,然后将栈中所有元素出栈去和链表中的比较(会有一半比较是多余的)。 也可以在入栈时记录个数,这样可以避免多余的比较。
5 设有两个栈s1、s2都采用顺序栈方式,并共享一个存储区
[0,...,maxsize-1]
,为了尽量利用空间,减少溢出的可能,可采用栈顶相向、迎面增长的存储方式。试设计s1、s2有关入栈和出栈的操作算法。
typedef struct{
Elemtype data[maxsize];
int top[2];
}
void InitStack(ShStack &S){
S.top[0] = -1;
S.top[1] = maxsize;
}
// i 指示栈号
bool push(ShStack &S, int i, Elemtype x){
if(S.top[0] + 1 == S.top[1])
return false;
if(i == 0)
S.data[++S.top[0]] = x;
else if(i == 1)
S.data[--S.top[1]] = x;
else
return false;
return true;
}
bool pop(ShStack &S, int i, Elemtype &x){
switch(i){
case 0: if(S.top[0] == -1)
return false;
x = S.data[S.top[0]--];
return true;break;
case 1: if(S.top[1] == maxsize)
return false;
x = S.data[S.top[1]++];
return true;
}
return false; // 栈号错误
}
应用
-
括号匹配
bool bracketCheck(char str[], int length){ SqStack S; // 这里使用顺序栈,可能会溢出哦。链栈更安全 char topElem; InitStack(S); for(int i = 0; i < length; i++){ if(str[i] == '(' || str[i] == '[' || str[i] =='{'){ Push(S,str[i]); }else{ if(StackEmpty(S)) // 栈空 return false; Pop(S,topElem); if(topElem != '(' && str[i] ==')') return false; if(topElem != '[' && str[i] ==']') return false; if(topElem != '{' && str[i] =='}') return false; } } return StackEmpty(S); }
三种错误情况:
- 括号不匹配
- 左括号数量大于右括号
- 右括号数量大于左括号
-
中缀表达式转后缀表达式 使用一个运算符栈
由于中缀表达式转后转表达式代码实现比较复杂,仅简要说明实现过程。
1
去除中缀表达式中的空白字符2
将中缀表达式进行拆分,分为操作数(整数),运算符,括号三种类型,存储可借助结构体3
借助栈实现中缀表达式到后缀表达式的转换
3.1
初始化一个运算符栈为空栈
3.2
从左到右扫描各个元素,直到末尾
3.2.1
遇到操作数,直接加入后缀表达式
3.2.2
遇到括号,遇到’(’ 直接入栈;遇到’)‘则依次弹出栈内运算符并加入后缀表达式,直到弹出’)'为止
3.2.3
遇到运算符,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到’('或栈空则停止。
之后再把当前运算符入栈。
3.3
若栈不为空,将运算符栈中所有符号依次加入到后缀表达式 -
根据后缀表达式计算中缀表达式 使用一个操作数栈
1
初始化一个空的操作数栈2
从左向右扫描后缀表达式的每一个元素,直到处理完所有的元素
2.1
若扫描到操作数,则压入栈,并转向2
2.2
若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压入栈 先出栈的为右操作数 -
中缀表达式的计算 使用一个符号栈,一个操作数栈
1
初始化两个栈,操作数栈和运算符栈2
从左向右扫描中缀表达式的每一个元素,直到处理完所有的元素
2.1
若扫描到操作数,压入操作数栈
2.2
若扫描到运算符或界限符,则按中缀转后缀的3.2
压入运算符栈,如果有运算符弹出,则需要弹出两个操作数栈的栈顶元素并执行 计算,运算结果再压回操作数栈
1
假设一个算法表达式中包含圆括号、方括号和花括号3中类型的括号,编写一个算法来判别表达式中的括号是否配对,以字符\0
作为算数表达式的结束符。
bool bracketCheck(char str[]){
SqStack S; // 这里使用顺序栈,可能会溢出哦。链栈更安全
char topElem;
InitStack(S);
for(int i = 0; str[i] != '\0'; i++){ // 其实直接写 str[i] 即可,因为'\0'转为整型即0,表示假
if(str[i] == '(' || str[i] == '[' || str[i] =='{'){
Push(S,str[i]);
}else if(str[i] == ')' || str[i] == ']' || str[i] =='}'){
if(StackEmpty(S)) // 栈空
return false;
Pop(S,topElem);
if(topElem != '(' && str[i] ==')')
return false;
if(topElem != '[' && str[i] ==']')
return false;
if(topElem != '{' && str[i] =='}')
return false;
}else
continue;
}
return StackEmpty(S);
}
// 王道给出的解法并没有考虑,左括号少于右括号的情况。
2
按下图所示铁道进行车厢调度(注意,两侧铁道均为单向行驶道,火车调度站有一个用于调度的“栈道”),火车调度站的入口处有n节硬座和软座车厢(分别用H和S表示)等待调度,试编写算法,输出对这节车厢进行调度的操作(即入栈或出栈操作)序列,以使所有的软座车厢都被调整到硬座车厢之前。
void Dispatch(char seat[], int n){
SqStack S;
InitStack(S);
for(int i = 0; i < n; i++){
if(seat[i] == 'H'){
Push(S,seat[i]);
printf("Push "); // 入栈
}else if(seat[i] == 'S'){
Push(S,seat[i]);
printf("Push "); // 入栈
Pop(S); // 出栈
printf("Pop");
}
}
while(!StackEmpty(S)){
Pop(S);
printf("Pop");
}
}
// 王道做法,思路一样,利用指针来做的
void Train_Arrange(char *train){
char *p = train, *q = train, c; // p,q两个指针同时操作着train,省空间
stack s;
InitStack(s);
while(*p){
if(*p == 'H')
Push(s,*p); // 硬座先放入栈,主要用于记录个数
else
*(q++) = *p; // 软座排在前面
p++;
}
while(!StackEmpty(s)){ // s中全是软座
Pop(s,c);
*(q++) = c; // 把H放在火车尾部
}
// 最终的train即是火车序列,,,感觉不符合题意,并没有输出调度的序列,如果只要火车序列,统计个数就行。
}
3
利用一个栈实现以下递归算法的非递归计算:
struct stack{
int nn; // 记录Pn(x) 的 n
double val; // 记录 Pn(x) 的值
}st[MaxSize];
int func(int n,int x){
int top = -1;
for(int i = 0; i <= n - 2; i++)
st[++top].nn = n - i; // 从栈顶到栈底为 2 ~ n
int v1 = 1, v2 = 2*x; // v1 = P0(x), v2 = P1(x);
while(top != -1){
st[top].val = 2*x*v2 - 2*(st[top].nn-1)*v1;
v1 = v2;
v2 = st[top].val;
top--;
}
if(n == 0)
return v1; // 易错,因为只有 n >= 2时才用到了栈,所以需要对 n = 0 和 n = 1特殊处理,n = 0时返回v1, n = 1时返回v2。
return v2;
}
4
某汽车轮渡口,过江渡船每次能载10辆车过江。过江车辆分为客车类和货车类,上渡船有如下规定:同类车先到先上船;客车先于货车上船,且每上4辆客车,才允许放上1辆货车;若等待客车不足4辆,则以货车代替;若无货车等待,允许客车都上船。试设计一个算法模拟渡口管理。
思路:由于客车优先级高,所以先看当前共有多少量车在等待(如虽然货车先到 第7个,客车排在后面 第11个,但我也有可能上船),所以先将所有的客车按顺序加入队列,所有的货车按顺序加入队列,客车队列不为空时,加入四辆客车,就加入一辆货车,如果没有货车,就全是客车。当队列为空时,剩下的全部加入货车。
void Port(char car[],int n){
// car代表车辆依次到来的种类,K表示客车,H表示火车
// n代表车的总数量
char *p = car, c;
queue k,h;
InitQueue(k);
InitQueue(h); // 初始化队列
for(int i = 0; i < n; i++){
if(car[i] == 'K')
EnQueue(k,'K');
else
EnQueue(h,'H');
}
int numk = 0, sum = 0;
while(sum < 10 && !QueueEmpty(k)){
DeQueue(k, c);
*(p++) = c;
numk++;
sum++;
if(numk % 4 == 0){
if(!QueueEmpty(h)){
Pop(h, c);
*(p++) = c;
sum++;
if(sum >= 10) break;
}
}
}
while(sum < 10 && !QueueEmpty(h)){
Pop(h,c);
*(p++) = c;
}
*(p) = '\0';
}
队列
循环队列
结构体定义
#define MaxSize 10
typedef struct{
int data[MaxSize];
int front,rear;
}SqQueue;
常见操作
void InitQueue(SqQueue &Q){
Q.front = Q.rear = 0;
}
// 将队列置空
void MakeEmpty(SqQueue &Q){
Q.front = Q.rear = 0;
}
// 入栈
bool EnQueue(SqQueue &Q, int x){
if(Q.front == (Q.rear + 1) % MaxSize)
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize;
return true;
}
// 出栈
bool DeQueue(SqQueue &Q, int &x){
if(Q.front == Q.rear)
return false;
x = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
return true;
}
// 获取头结点
bool GetHead(SqQueue Q, int &x){
if(Q.front == Q.rear)
return false;
x = Q.data[Q.front];
return true;
}
// 判空
bool Empty(SqQueue Q){
return Q.front == Q.rear;
}
🐯 判断队空和队满的三种形式
rear
指向对尾元素的下一个位置,front
指向队头元素,牺牲一个单元来区分队空和队满
队空:rear == front
队满:(rear + 1) % MaxSize == front
− − > --> −−>MaxSize
指代共有多少个元素,数组下标从0 ~ MaxSize - 1
队中元素的个数:(rear + MaxSize - front) % MaxSize
- 增设表示元素个数的数据成员
size
队空:size == 0
队满:size == MaxSize
- 增设数据成员
tag
,进行出队操作时,置tag = 0
,进行入队操作时,置tag = 1
原理:只有出队才会引起队空,入队才会引起队满
队空:front == rear && tag == 0
队满:front == rear && tag == 1
队中元素的个数:if(rear == front && tag == 1) size = MaxSize; else size = (rear + MaxSize - front) % MaxSize;
初始化时置tag = 0;
,因为初始时rear == front
且此时队列为空
rear也可以指向队尾元素,front指向对头元素,如此,在每次进行入队操作时,
rear
要先加1取余,再插入元素。同时,如果想在初始时第一个元素在A[0]位置插入,那么初始化时应rear = MaxSize - 1
,这样在第一个元素加入时,有rear = (rear + 1) % MaxSize
,即为0。
链队列
🐯 含头结点
结构体定义
typedef struct LNode{
int data;
struct LNode *next;
}LNode;
typedef struct{
LNode *front, *rear;
}LinkQueue;
常用操作
// 初始化
void InitQueue(LinkQueue &Q){
Q.rear = Q.front = (LNode*) malloc(sizeof(LNode));
(Q.front)->next = NULL;
}
// 销毁
void DestroyQueue(LinkQueue &Q){
LNode *p;
while(Q.front != Q.rear){
p = Q.front;
Q.front = p->next;
free(p);
}
free(Q.front);
Q.front = Q.rear = NULL; // 可以加上,安全
}
// 进队
bool EnQueue(LinkQueue &Q, int x){
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = x;
s->next = NULL;
Q.rear->next = s;
Q.rear = s;
return true;
}
// 出队
bool DeQueue(LinkQueue &Q, int &x){
if(Q.front == Q.rear)
return false;
LNode *u = Q.front->next;
x = u->data;
Q.front->next = u->next;
if(Q.rear == u) // 出队的是最后一个结点
Q.rear = Q.front;
free(u);
return true;
}
// 获取队头元素
bool GetHead(LinkQueue Q, int &x){
if(Q.front == Q.rear)
return false;
x = Q.front->next->data;
return true;
}
// 判空
bool Empty(LinkQueue Q){
return Q.front == Q.rear;
// return Q.front->next = NULL;
}
进队、出队因为要修改LinkQueue中的rear,front,所以也要加引用
🐯 不含头结点
结构体定义同上
常见操作
// 初始化
void InitQueue(LinkQueue &Q){
Q.rear = Q.front = NULL;
}
// 销毁
void DestroyQueue(LinkQueue &Q){
if(Q.rear == NULL) return; // 空队列
LNode *p = Q.rear, *u;
while(p){
u = p;
p = p->next;
free(u);
}
Q.rear = Q.front = NULL;
}
// 入队
bool EnQueue(LinkQueue &Q, int x){
LNode *s = (LNode*) malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = x;
s->next = NULL;
if(Q.rear == NULL)
Q.rear = Q.front = s;
else{
Q.rear->next = s;
Q.rear = s;
}
return true;
}
bool DeQueue(LinkQueue &Q, int &x){
if(Q.front == NULL) // 空队列
return false;
LNode *u = Q.front;
x = u->data;
Q.front = u->next;
if(Q.rear == u)
Q.front = Q.rear = NULL;
free(u);
return true;
}
bool GetHead(LinkQueue Q, int &x){
if(Q.front == NULL)
return false;
x = Q.front->data;
return true;
}
bool Empty(LinkQueue Q){
return Q.front == Q.rear;
// return Q.front == NULL;
// return Q.rear == NULL;
}
算法设计
1 若希望循环队列中的元素都能得到利用,则需设置一个标志域
tag
,并以tag
的值为0或1来区分队头指针front
和队尾指针rear
相同时的队列状态是“空”还是“满”。试编写与此结构相应的入队或出队算法。
InitQueue(SqQueue &Q){
Q.tag = 0; // 初始化tag为0,保证初始时 front == rear 指示队列为空
front = rear = 0;
}
// 入队
bool EnQueue(SqQueue &Q, int x){
if(Q.front == Q.rear && tag == 1)
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize;
Q.tag = 1;
return true;
}
// 出队
bool DeQueue(SeQueue &Q, int &x){
if(Q.front == Q.rear && tag == 0)
return false;
x = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
Q.tag = 0;
return true;
}
// 最后不要忘记对tag进行修改
2 Q是一个队列,S是一个空栈,实现将队列中的元素逆置的算法。
void Reverse(Queue &Q, Stack &S){
Elemtype x;
while(!QueueEmpty(Q)){
DeQueue(Q,x);
Push(S,x);
}
while(!StackEmpty(S)){
Pop(S,x);
EnQueue(Q,x);
}
}
// 写法2
void Reverse(Queue &Q, Stack &S){
Elemtype x;
while (DeQueue(Q, x))
Push(S, x);
while (Pop(S, x))
EnQueue(Q, x);
}
3 利用两个栈S1和S2来模拟一个队列,已知栈的4个运算定义如下:
Push(S,x); // 元素x入栈S
Pop(S,x); // S出栈并将栈的值赋给x
StackEmpty(S); // 判断栈是否为空
StackOverflow(S); // 判断栈是否满
利用栈的运算来实现队列的3个运算,包括入队,出队,判断队列是否为空。
思路:栈可以实现一次逆序,两个栈可以实现两次逆序。即正序,一个用于入队时使用,一个用于出队时使用。
bool Enqueue(Stack &S1, Stack &S2, int x){
Elemtype y;
// 栈1未空,可直接插入
if(!StackOverflow(S1)){
Push(S1,x);
return true;
}
// 栈1满,如果栈2非空,那栈1元素不能放入S2,否则就乱序了
if(StackOverflow(S1) && !StackEmpty(S2))
return false;
// 栈1满,栈2空,则栈1的元素可且必须全部放入栈2
if(StackOverflow(S1) && StackEmpty(S2)){
while(!StackEmpty(S1)){
Pop(S1,y);
Push(S2,y);
}
}
Push(S1,x);
return true;
}
bool DeQueue(Stack &S1, Stack &S2, int &x){
Elemtype y;
// 栈2非空,可直接取出
if(!StackEmpty(S2)){
Pop(S2,x);
return true;
}
// 栈1,栈2均为空
if(StackEmpty(S1) && StackEmpty(S2))
return false;
// 栈2为空,栈1不为空 此处的StackEmpty(S2)可以不用加入判断
while(StackEmpty(S2) && !StackEmpty(S1)){
while(!StackEmpty(S1)){
Pop(S1,y);
Push(S2,y);
}
}
Pop(S2,x);
return true;
}
bool QueueEmpty(Stack &S1, Stack &S2){
return StackEmpty(S1) && StackEmpty(S2);
}
串
顺序存储
结构体定义
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
typedef struct{
char *ch;
int length;
}HString;
🐯 四种顺序存储方案
- 字符存储从下标0开始,并设置
length
变量 - 字符存储从下标1开始,使用
ch[0]
记录串长,不过只能记录0~255
长度 - 字符存储从下标0开始,使用
\0
作为结尾,计算串长的时间复杂度为O(n) - 字符存储从下标1开始,使用
length
变量,且ch[0]
弃置不用
其中 2,4可让字符的位序与数组下标相同。以下算法都是用第4种来实现
常用操作
求子串
bool SubString(SString &Sub, SString S, int pos, int len){
if(pos + len - 1 > S.length)
return false;
for(int i = pos; i < pos + len; i++){
Sub.ch[i - pos + 1] = S.ch[i];
}
Sub.length = len;
return true;
}
串比较
int StrCompare(SString S, SString T){
for(int i = 1; i <= S.length && i <= T.length; i++)
if(S.ch[i] != T.ch[i])
return S.ch[i] - T.ch[i];
return S.length - T.length; // 扫描的所有字符都相等,那就按长度返回
}
// 返回值 > 0 --> S > T
// 返回值 = 0 --> S = T
// 返回值 < 0 --> S < T
定位操作 --> 朴素模式匹配算法的一种实现
int Index(SString S, SString T){
int j;
for(int i = 1; i <= S.length - T.length + 1; i++){
for(j = 1; j <= T.length; j++){
if(S.ch[i + j - 1] != T.ch[j])
break;
}
if(j > T.length)
return i;
}
return 0; // 因为串的下标从1开始,所以返回0,代表每匹配到
}
链式存储
typedef struct StringNode{
char ch;
struct StringNode *next;
}StringNode, *String; // 存储密度高
// 推荐
typedef struct StringNode{
char ch[4]; // 最后一个结点如果不足四个字符,可用 '#','\0'填充
struct StringNode *next;
}StringNode, *String;
串的模式匹配
朴素模式匹配算法
时间复杂度 O ( m n ) O(mn) O(mn)
int Index(SString S, SString T){
int i = 1, j = 1;
while(i <= S.length && j <= T.length){
if(S.ch[i] == T.ch[j]){
i++;
j++;
}else{
i = i - j + 2; // 从i开始(包括i)有j-1个与字串相同的字符,所以是从 i-(j-1)位置开始匹配,再加1即是开始位置的下一个位置
// 即 i-(j-1)+1 = i-j+2
j = 1;
}
}
return j > T.length ? i - T.length : 0;
}
KMP算法
朴素模式匹配算法再每次匹配不成功后都 i 从起始位置的下一位置重新开始,没有利用原来已经比较过的信息 --> KMP算法出现
如原来的匹配模式
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|
主串S | A | B | A | A | B | B | * | * |
模式串T | A | B | A | A | B | C |
在匹配到下标为 6 位置时,出现了不匹配字符,于是,按照朴素模式和匹配算法要从2位置开始重新匹配
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|
主串S | A | B | A | A | B | B | * | * |
模式串T | A | B | A | A | B | C |
KMP算法的思想是,既然我还没有找到匹配串,那从未匹配处之前,如上述6位置,都是匹配的,且主串 1~5
与模式串 1~5
都是一样的,接下来按照朴素模式匹配算法,我又让主串2~5
与模式串 1~4
比较。其实这种是多余的,因为我知道 主串 2~5
实际上等于模式串 2~5
,相当于我每次都会让一个模式串子串与模式串进行匹配。所以朴素模式匹配算法低效的原因正是这种大量重复且结果固定的匹配存在。
❓ 如何消除
利用模式串本身来找规律。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|
主串S | A | B | A | A | B | B | * | * |
模式串T | A | B | A | A | B | C |
当我在第 6 位不匹配时
第一步:模式串后移一位,比较,发现第 2 位不匹配
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|
主串S | A | B | A | A | B | B | * | * |
模式串T | A | B | A | A | B | C |
第二步:模式串后移一位,比较,发现第 4 位不匹配
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|
主串S | A | B | A | A | B | B | * | * |
模式串T | A | B | A | A | B | C |
第三步:模式串后移一位,比较,来到第一次发现不匹配的位置,对第 6 位重新判别
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|
主串S | A | B | A | A | B | B | * | * | * |
模式串T | A | B | A | A | B | C |
实际上,第一步和第二步是多余的,对我们来说第三步才是我们要的,我们能否利用已知条件消除这些呢?
实际上,第一步和第二步的比较都相当于模式串自身在比较,如第一步:2~5
与 1~4
比较,第二步:3~5
与 1~3
比较,第三步:4~5
与 1~2
比较,找到规律 – > 模式串前 k 位 与模式串后 k 位的比较
模式串是已知的,所以,如果我们提前计算出前 k 位与后 k 位是否相等,是不是就可以跳过某些步骤,如上述第一、二步,比较后发现不相等,而在第三步相等了,才开始对第 6 位的重新判别。
若在第N位发生不匹配,要从模式串 1 ~ N - 1位找到满足前K个与后K个字符相同的最大的K来进行移动,因为K越大,移动的位数越少。
下面,我们试着来找一下,并假设在第 n 个位置发生不匹配。i 指代主串下标,j指代模式串下标,均从1开始。
当在某个位置主串与模式串匹配时
i++; j++;
在第 1 个位置发生不匹配
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | B | * | * | * | * | * |
模式串T | A | B | A | A | B | C |
主串指针后移一位,模式串还是从1开始,i = i + 1, j = 1;
在第 2 个位置发生不匹配
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | A | * | * | * | * |
模式串T | A | B | A | A | B | C |
A的前1个元素和A的后一个元素相同,大小为1
模式串 1 位置与 主串 2 位置开始比较,i = i, j = 1;
即 i 不变,j右移一个位置,如下。
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | A | * | * | * | * |
模式串T | A | B | A | A | B |
在第 3 个位置发生不匹配
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | B | B | * | * | * |
模式串T | A | B | A | A | B | C |
AB并没有相同前后缀,大小为0
i 不变,j后移三个位置到4,此时j为1
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | B | B | * | * | * |
模式串T | A | B | A | A |
在第 4 个位置发生不匹配
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | B | A | B | * | * |
模式串T | A | B | A | A | B | C |
ABA有相同前后缀A,大小为1,i不动,此时j为2
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | B | A | B | * | * |
模式串T | A | B | A | B |
在第 5 个位置发生不匹配
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | B | A | A | A | * |
模式串T | A | B | A | A | B | C |
ABAA有相同前后缀A,大小为1,i不动,此时j为2
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | B | A | A | A | * |
模式串T | A | B | A |
在第 6 个位置发生不匹配
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | B | A | A | B | B |
模式串T | A | B | A | A | B | C |
ABAAB有相同前后缀AB,大小为2,i不动,此时j为3
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
主串S | A | B | A | A | B | B |
模式串T | A | B | A |
由此可总结出表格
在模式串j位置处发生不匹配 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
j的下一个值 | 0 | 1 | 1 | 2 | 2 | 3 |
总结规律
在第 n 个位置发生不匹配
- n == 1
j = 1, i = i + 1;
可修改为j = 0; i++; j++;
- n == 2
j = 1, i = i;
- n为其他值,找模式串前 n - 1 个字符,满足前k个与后k个字符相同的最大k值,
j = k + 1;
以上可叙述为 next[1] = 0,next[2] = 1,next[other] = k + 1;
由此,可得出 KMP 匹配算法。
int Index_KMP(SString S, SString T, int next[]){
int i = 1, j = 1;
while(i <= S.length && j <=T.length){
if(j == 0 || S.ch[i] == T.ch[j]){
i++;j++; // 继续比较后面字符
}else
j = next[j]; // 修改j的下标,向右移动
}
if(j > T.length)
return i - T.length;
return 0;
}
🐯 next数组求解
所以现在又来到了next数组的求解
我们已有如下结论,关键在于 k 的求解如何用代码实现呢?
next[1] = 0,next[2] = 1,next[other] = k + 1;
我们来分析以下,给出模式串
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
模式 | A | B | A | A | B | C | A | B |
next[j] | 0 | 1 |
如果 已知 P 1 . . . P k − 1 = P j − k + 1 . . . P j − 1 P_1...P_{k-1} = P_{j-k+1}...P_{j-1} P1...Pk−1=Pj−k+1...Pj−1
-
P k = P j P_k = P_j Pk=Pj
则
next[j + 1] == k + 1 == next[j] + 1;
-->next[j] == (k - 1) + 1;
-
P k ! = P j P_k != P_j Pk!=Pj
其实这里本质和上面我们在分析KMP算法,某一位发生不匹配是一模一样的,都是模式串的比较,因为第 k 位的加入做了新的末尾,所以我们要去找头 m 个 与 尾 m 个相同的最大 m 值。即j = next[j];
,直到 P k = P j P_k = P_j Pk=Pj (发现了吗,在KMP算法中,如果我们已知next数组就可以很轻松的进行j的修改,而在求解next数组时,我们也在使用KMP算法,使用前 k-1 位模式串已求出的next数组) 。如果不理解,可以对模式串进行一次朴素模式匹配,回到起点。
void get_next(SString T, SString next[]){
int i = 1, j = 0;
next[1] = 0;
while(i < T.length){
if(j == 0 || T.ch[i] == T.ch[j]){
// 除了第一个位置 next[1] = 0, 其他位置没有相等前后缀,next[j] 都等于 1,所以j=0时进入
// 这里也是 i 只向前走
i++; j++;
next[i] = j;
// 当Pi = Pj时,next[j + 1] = next[i] + 1; 而 next[i] 就是 next[j] 因为此处
// 主串和模式串一模一样, 所以 next[i + 1] = next[j] + 1 实际上(next[i + 1] = j + 1)
// 因为在这里 j 是等于 next[j]的,这就是KMP中j的含义,j指代下一次模式串的位置,也等于
// 公共前后缀的长度k + 1, k即前i-1位的公共前后缀。所以如果不进行i++,j++; 就
// 等价于 next[i] = next[j] (next[j] == j); 执行后 ++操作 就是我们需要的
// next[i + 1] = j + 1;
}else
j = next[j];
}
}
🐯 KMP的优化
如下面模式串
j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
模式串 | A | A | A | A | B |
next[j] | 0 | 1 | 2 | 3 | 4 |
场景:
AAAB*
AAAAB
在进行比较时,第四位不匹配,根据next[j],模式串移到 j = 3的位置。
而在4位置处不匹配,我们可以直到主串该位置处必不为A,而模式串移动到 j = 3的位置,此处模式串也为A,肯定也不匹配,同理,又到2位置,1位置,浪费很多时间。
所以如果 P j = P n e x t [ j ] P_j = P_{next[j]} Pj=Pnext[j] ,那么让 n e x t [ j ] = n e x t [ n e x t [ j ] ] next[j] = next[next[j]] next[j]=next[next[j]],一直找到 P j ! = P n e x t [ j ] P_j != P_{next[j]} Pj!=Pnext[j]
如果 P j ! = P n e x t [ j ] P_j != P_{next[j]} Pj!=Pnext[j],那么不需要改动,所以我们只需在原来的基础上修改代码
void get_next(SString T, SString next[]){
int i = 1, j = 0;
next[1] = 0;
while(i < T.length){
if(j == 0 || T.ch[i] == T.ch[j]){
i++; j++;
if(T.ch[i] != T.ch[j])
next[i] = j;
else
next[i] = next[j]; // 一次就行,因为是从前往后计算的next数组,前面的都满足 Pj != Pnext[j];
}else
j = next[j];
}
}
树
二叉树
结构体定义
- 顺序存储结构
struct TreeNode{
ElemType value;
bool isEmpty; // 标识该位置是否有数据
};
TreeNode t[MaxSize];
// 初始化
void InitTree(TreeNode t[]){
for(int i = 0; i < MaxSize; i++)
t[i].isEmpty = true;
}
适合于完全二叉树和满二叉树。
-
链式存储结构
- 二叉链表
struct ElemType{
int value;
}; // 这种定义方式也挺好,将数据部分的定义单独使用一个结构体(如果数据较多时)
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
- 三叉链表
struct ElemType{
int value;
};
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild, *rchild;
struct BiTNode *parent;
}BiTNode, *BiTree;
常用操作
递归遍历二叉树
-
先序遍历
void PreOrder(BiTree T){ if(T != NULL){ visit(T); PreOrder(T->lchild); PreOrder(T->rchild); } }
-
中序遍历
void InOrder(BiTree T){ if(T != NULL){ InOrder(T->lchild); visit(T); InOrder(T->rchild); } }
-
后序遍历
void PostOrder(BiTree T){ if(T != NULL){ PostOrder(T->lchild); PostOrder(T->rchild); visit(T); } }
非递归遍历二叉树
–> 借助栈的思想进行实现
-
先序遍历
void PreOrder(BiTree T){ SqStack S; InitStack(S); BiTNode *p = T; while(p != NULL || !StackEmpty(S)){ if(p){ visit(p); Push(S,p); p = p->lchild; }else{ Pop(S,p); // p结点及p结点左子树都被访问过了 p = p->rchild; } } }
-
中序遍历
void InOrder(BiTree T){ SqStack S; InitStack(S); BiTNode *p = T; while(p != NULL || !StackEmpty(S)){ // 如果栈为空说明没有未被访问的结点,当 p == NULL && StackEmpty(S) == true 说明,最后一个结点被访问过了 // 如果仅p == NULL而栈不为空,说明某个结点左子树或右子树访问完了,要进行回溯 // 如果 p != NULL而栈为空,说明当前正在访问根结点右子树的某个右子树(因为只有左子树还未被访问的才会入栈) if(p){ Push(S,p); p = p->lchild; }else{ Pop(S,p); // p结点左子树都被访问过了 visit(p); p = p->rchild; } } }
-
后序遍历
- 方法一:额外使用一个栈来记录当前栈顶结点被访问的是左子树还是右子树
void PostOrder(BiTree t){ Stack vis; Stack nd; InitStack(vis); InitStack(nd); BiTNode *p = t, *q; while(p != NULL || !StackEmpty(nd)){ if(p){ Push(vis,'l'); // 向左访问 Push(nd,p); p = p->lchild; }else{ if(Top(vis) == 'r'){ // 栈顶结点的右孩子被访问过了,则访问当前结点 q = Top(nd); visit(q); Pop(nd); Pop(vis); }else{ p = Top(nd)->rchild; // 访问栈顶结点的右孩子 Pop(vis); Push(vis,'r'); } } } }
- 方法二:用一个指针变量来记录最近访问过的结点。
void PostOrder(BiTree T){ Stack S; InitStack(S); BiTNode *p = T, *r = NULL; while(p || !StackEmpty(S)){ if(p){ Push(S,p); p = p->lchild; }else{ GetTop(S,p); if(p->rchild && p->rchild != r) // 右孩子结点未被访问,若p->rchild == NULL 表示没有右孩子,若p->rchild == r,表示右孩子被访问过了。 p = p->rchild; else{ // 栈顶的右孩子已被访问,则访问栈顶结点,并设置 r = p; // 因为要访问当前结点一定要先访问右孩子结点,若要访问该结点,则右孩子要么为空,要么右孩子已被访问过。 // p->rchild == NULL || p->rchild == r 入栈顺序-->右孩子会在当前结点的上面,所以右孩子被访问后令 r = p Pop(S,p); visit(p); r = p; // 此处 r = p; 如果当前访问的结点是父结点的右孩子结点,返回到父结点后,会判断出 p->rchild == r,即出栈 p = NULL; // 当前结点及左右子树被访问完啦,接着去栈顶取,很关键 } } } } // 二狗写法 void PostOrder2(BiTree T) { Stack S; InitStack(S); BiTNode *p = T; BiTNode *r = NULL; // 初始化为NULL,为第1个结点的访问做准备 do{ while(p){ Push(S,p); p = p->lchild; } while(!StackEmpty(S)){ p = Top(S); if(p->rchild == NULL || p->rchild == r){ // 从右子树返回的 Pop(S,p); visit(p); r = p; }else{ // 从左子树返回的 p = p->rchild; // p的右子树为空或者右子树还未被访问 break; } } }while(!StackEmpty(S)); }
- 方法三:不适用额外的栈,而是在树结点定义时设置一个
tag
标志访问哪个孩子被访问了
// 王道写法 typedef struct{ BiTNode *t; int tag; // tag = 0表示左子树被访问了,tag = 1表示右子树被访问了 }stack; void PostOrder(BiTree bt){ stack s[]; top = 0; while(bt != NULL || top > 0){ while(bt != NULL){ // 沿左分支遍历 s[++top].t = bt; s[top].tag = 0; bt = bt->lchild; } while(top != 0 && s[top].tag == 1){ visit(s[top]); top--; } if(top != 0){ // 沿右分支向下 s[top].tag = 1; bt = s[top].t->rchild; } } }
层次遍历二叉树
–> 借助队列
// 队列定义
typedef struct LinkNode{
BiTNode *data; // 存指针而不是存结点,更省空间
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue(Q);
BiTNode *p;
EnQueue(Q,T);
while(!QueueEmpty(Q)){
DeQueue(Q,p);
visit(p);
if(p->lchild != NULL)
EnQueue(Q,p->lchild);
if(p->rchild != NULL)
EnQueue(Q,p->rchild);
}
}
求树的深度
int TreeDepth(BiTree T){
if(T != NULL){
int l = TreeDepth(T->lchild);
int r = TreeDepth(T->rchild);
return l > r ? l + 1 : r + 1;
}else
return 0;
}
线索二叉树
结构体定义
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志,为0,则指针域指向孩子, 为1时,ltag指向前驱,rtag指向后继
}ThreadNode, *ThreadTree;
常用操作
-
建立中序线索二叉树
void CreateInThread(ThreadTree T){ ThreadNode *pre = NULL; if(T != NULL){ InThread(T,pre); // 结束后,pre是指向最后一个结点的 // 此处也可以不用判断右孩子是否为空,因为一定是空的 if(pre->rchild == NULL) pre->rtag = 1; // 置rtag为1,没有后继 } } void InThread(ThreadTree T, ThreadNode &pre){ if(T != NULL){ // 访问左孩子 InThread(T->lchild,pre); // 访问当前结点 if(p->lchild == NULL){// 设置当前结点的前驱 p->ltag = 1; p->lchild = pre; } if(pre != NULL && pre->rchild == NULL){ // pre != NULL 要加上,因为第一次进入的时候pre == NULL pre->rtag = 1; pre->rchild = p; } // 访问当前结点时设置上一个结点的后继 pre = p; // 访问右孩子 InThread(T->rchild,pre); } }
-
求中序线索二叉树中序遍历下的第一个结点
ThreadNode *FirstNode(ThreadTree *T){ while(T->ltag == 0) T = T->lchild; return T; }
-
求中序线索二叉树中序遍历下的最后一个结点
ThreadNode *LastNode(ThreadTree *T){ while(T->rtag == 0) T = T->rchild; return T; }
-
求中序线索二叉树某个结点的后继
ThreadNode *NextNode(ThreadNode *p){ if(p->rtag == 1) p = p->rchild; else{ p = p->rchild; while(p->ltag == 0) p = p->lchild; } return p; }
-
求中序线索二叉树某个结点的前驱
ThreadNode *PriorNode(ThreadNode *p){ p = p->lchild; if(p->ltag != 1) while(p->rtag == 0) p = p->rchild; return p; }
-
建立先序线索二叉树
void CreatePreThread(ThreadTree T){ ThreadNode *pre = NULL; if(T != NULL){ PreThread(T,pre); if(pre->rchild == NULL) pre->rtag = 1; // 置rtag为1,没有后继 } } void PreThread(ThreadTree T, ThreadNode &pre){ if(T != NULL){ // 访问当前结点 if(p->lchild == NULL){ p->ltag = 1; p->lchild = pre; } if(pre != NULL && pre->rchild == NULL){ pre->rtag = 1; pre->rchild = p; } pre = p; // 访问左孩子 if(T->ltag == 0) // 没有被线索化 --> 和其他两种线索化不同的一个地方 PreThread(T->lchild,pre); // 访问右孩子 PreThread(T->rchild,pre); } }
-
建立后序线索二叉树
void CreatePostThread(ThreadTree T){ ThreadNode *pre = NULL; if(T != NULL){ PostThread(T,pre); if(pre->rchild == NULL) pre->rtag = 1; // 置rtag为1,没有后继 } } void PostThread(ThreadTree T, ThreadNode &pre){ if(T != NULL){ // 访问左孩子 PreThread(T->lchild,pre); // 访问右孩子 PreThread(T->rchild,pre); // 访问当前结点 if(p->lchild == NULL){ p->ltag = 1; p->lchild = pre; } if(pre != NULL && pre->rchild == NULL){ pre->rtag = 1; pre->rchild = p; } pre = p; } }
树、森林
树、森林的存储
-
双亲表示法
示意图
结构体定义
#define MAX_TREE_SIZE 100
typedef struct{ // 树的结点定义
Elemtype data;
int parent;
}PTNode;
typedef struct{ // 树的结构体定义
PTNode nodes[MAX_TREE_SIZE]; // 双亲表示
int n; // 结点数
}PTree;
<font title = "blue">常用操作</font>
```c++
// 删除叶节点
// 方案1(不推荐)
void deleteNode(PTree &t, int i){
t.nodes[i].parent = -1;
t.n--;
}
// 方案2
void deleteNode(PTree &t, int i){
t.node[i].parent = t.node[n - 1].parent;
t.node[i].data = t.node[n - 1].data;
t.n--;
}
// 删除非叶子结点
以该结点为根结点的整棵子树都要删除,需要用到查询操作
// 插入
// 如果选择第二种删除方式,那么插入只需要在末尾插入即可,n不仅代表结点数,还能指示下一个结点插入位置
// 如果选择第一种,那么数组中会有空结点,在插入时最好去遍历数组,找空结点插入,但此时有两个弊端
// 1. 根结点和空结点均 parent == -1,要进行区分
// 2. n不再能指示下一个结点的插入位置
bool insertNode(PTree &t, Elemtype data, int parent){
if(t.n >= MAX_TREE_SIZE)
return false;
PTNode p;
p.data = data;
p.parent = parent;
t.nodes[n++] = p;
return true;
}
// 查找
// 如果选择了第一中删除方式,在遍历过程中也会因为空数据导致遍历更慢
优点:找指定结点的双亲结点很方便
缺点:查找指定结点的孩子只能从头遍历
-
孩子表示法
示意图
结构体定义
struct CTNode{
int child; // 孩子结点在数组中的位置
struct CTNode *next; // 下一个孩子
};
typedef struct{
Elemtype data;
struct CTNode *firstChild; // 第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n, r; // 结点数和根的位置
}
<font title = "blue">常用操作</font>
```c++
// 删除
1. 找到父结点,将其从父结点的孩子链表中删除
2. 删除以该结点为根结点的子树(删除该结点孩子链表中的所有孩子,firstChild置为NULL) --> 递归法
// 插入
1. 在数组中找到一个空的区域,将数据存入,记录下标,同时n++
2. 在父结点的孩子链表中存入该孩子的下标
// 查询
该存储结构适合层次遍历
查询时借助队列比较方便
-
孩子兄弟表示法
结构体定义typedef struct CSNode{ Elemtype data; struct CSNode *firstchild, *nextsibling; // 第一个孩子和右兄弟指针 }CSNode, *CSTree;
对于森林的存储,使用孩子兄弟表示法,森林中每棵树的根结点之间互相看作兄弟。
🐯 小技巧
- 二叉树的根结点若有右孩子,说明是由森林转化过来的,若只有左孩子或只有根结点,则是由树转化而来的
- 二叉树若有右孩子,且一直向右的孩子数目即森林中树的个数。如下图B有右孩子C,C有右孩子D,D无右孩子,所以森林中有三个树,根结点分别为B,C,D
树和森林的遍历
1. 树的遍历
// 先根遍历
void PreOrder(TreeNode *R){
if(R){
visit(R);
while(R还有下一棵子树T)
PreOrder(T); // 先序遍历下一棵子树
}
}
树的先根遍历与对应二叉树的先根遍历序列相同
// 后根遍历
void PostOrder(TreeNode *R){
if(R){
while(R还有下一棵子树T)
PostOrder(T); // 后序遍历下一棵子树
visit(R);
}
}
树的后序遍历与对应二叉树的中序遍历序列相同
❓ 为什么树的先序与二叉树的先序相同,而树的后序与二叉树的中序相同?
下面解释只需要看图中的ABCD结点即可。
-
树的先根遍历与对应二叉树的先根遍历序列相同
首先要直到二叉树中结点的含义,每个结点的左孩子即以该结点为根的完整子树,每个结点的右孩子是兄弟结点。
如下图,先序遍历树时, A -> B(所在子树) -> C(所在子树) -> D(所在子树)。
在转为二叉树时,会变为 A -> B(所在子树) -> C(B的右孩子,C所在子树) -> D(C的右孩子,D所在子树)
二者先序遍历的序列是相同的,而树也是递归定义的,所以所有子树也都适用,故二者遍历序列相同。 -
树的后序遍历与对应二叉树的中序遍历序列相同
后序遍历树时, B(所在子树,第一个孩子) -> C(所在子树,第二个孩子) -> D(所在子树,第三个孩子) -> A。
在转为二叉树后,由于A结点的所有子孙结点都在A的左子树上,且A的第一个孩子是A的左孩子,其余孩子会依次排在左兄弟的右孩子上。
因此对 A B C D应是中序遍历。即先遍历A的左孩子,再遍历完A的左孩子的左子树后(即A的第一个孩子结点所在子树被遍历完),再去遍历左孩子节点的右子树,就会把A的剩余孩子遍历完。
2. 森林的遍历
-
先序遍历
若森林非空,进行如下遍历1
访问森林中第一棵树的根结点。2
先序遍历第一棵树中根结点的子树森林。3
先序遍历除去第一棵树之后剩余的树构成的森林。🐯 tips
- 效果等同于依次对各个树进行先根遍历
- 效果等同于对二叉树进行先序遍历
-
中序遍历
若森林非空,进行如下遍历
1
中序遍历森林中第一棵树的根结点的子树森林。2
访问森林中第一棵树的根结点。3
中序遍历除去第一棵树之后剩余的树构成的森林。
🐯 tips- 效果等同于依次对各个树进行后根遍历 --> 根据定义分析是对各个树进行后序遍历
- 效果等同于对二叉树进行中序遍历
!!牢牢记住
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
🐯 常用结论
F 是一个森林,B是由F变换来的二叉树。
- 若F有n个非终端结点,则B中右指针域为空的结点有
n + 1
个
证明: 右指针域为空,说明该结点为父结点的最后一个孩子,即该结点没有右兄弟。 森林中的最后一棵树的右指针域为空,即1
,对于每个非终端结点,其最后一个孩子没有右兄弟,即右指针域为空,故共n
个加起来n + 1
个。
证明2: 设F中共有结点t
个,则在 B 中,结点个数也等于t
,设右指针域不为空的共m
个,又非终端结点指示 B 中左孩子结点不为空的结点有n
个,由二叉树的性质可知1 + n + m = t
,那么右指针为空的应为t - m = n + 1
个。 - F中叶结点的个数等于B中左孩子指针为空的结点个数
证明: 在森林中,如果某个结点没有孩子结点,那么转为二叉树后左孩子结点是为空的。
算法设计
5.2
5 已知一棵二叉树按顺序存储结构进行存储,设计一个算法,求编号分别为
i
和j
的两个结点的最近的公共祖先结点的值。
bool findCommonParent(SqTree T, int i, int j, ElemType &fa){
if(T[i] == '#' || T[j] == '#')
return false;
// 结点都存在 --> 必有公共祖先结点,根节点一定是
while(i != j){
if(i > j)
i = i / 2;
else
j = j / 2;
}
fa = T[i];
return true;
}
// 王道书
// 如果 (T[i] == '#' || T[j] == '#') 是没有返回值的
ElemType findCommonParent(SqTree T, int i, int j){
if(T[i] != '#' || T[j] != '#'){
while(i != j){
if(i < j)
i = i / 2;
else
j = j / 2;
}
return fa = T[i];
}
}
5.3
1 2 总结
前序遍历时共n个结点,假设为3个,(A,B,C),问有多少种符合如下条件的二叉树
- 前序 遍历序列 与 后序 遍历序列相反,共 2 n − 1 2^{n-1} 2n−1 种
NLR = ^(LRN) ==> NLR = NRL 得 R 为空(NL = NL) 或 L为空(NR = NR) ==> 除叶结点外,每个结点只有一个孩子,左右都可以- 前序 遍历序列 与 后序 遍历序列相同,当 n = 1 n = 1 n=1 时有 1 1 1 种,其他情况下不存在
NLR = LRN ==> LR为空,所以无左右子树,只有一个根结点。- 前序 遍历序列 与 中序 遍历序列相同,共 1 1 1 种
NLR = LNR ==> L为空,即只有右子树,一条线。- 前序 遍历序列 与 中序 遍历序列相反,共 1 1 1 种
NLR = ^(LNR) ==> NLR = RNL, R为空,即只有左子树,一条线。- 中序 遍历序列 与 后序 遍历序列相同,共 1 1 1 种
LNR = LRN ==> R为空,即只有左子树,一条线。- 中序 遍历序列 与 后序 遍历序列相反,共 1 1 1 种
LNR = ^(LRN) ==> LNR = NRL, L为空,即只有右子树,一条线。
3 编写后序遍历二叉树的非递归算法 --> 前面已给出
4 试给出二叉树的自下而上、从左到右的层次遍历算法
思路:利用队列和栈共同实现,先使用队列,将结点的右孩子先放入,再放入左孩子(实现从上到下,从右到左),出队后的结点放入栈(使用栈进行逆序)。 同王道思路
void LevelOrder(BiTree T){
if(T == NULL) return;
Stack S;
Queue Q;
InitStack(S);
InitQueue(Q);
EnQueue(Q,T);
while(!QueueEmpty(Q)){
DeQueue(Q,p);
Push(S,p);
if(p->rchild)
EnQueue(Q,p->rchild);
if(p->lchild)
EnQueue(Q,p->lchild);
}
while(!StackEmpty(S)){
Pop(S,p);
visit(p);
}
}
5 假设二叉树采用二叉链表存储结构,设计一个非递归算法求二叉树的高度。
思路1:遍历二叉树,前、中、后序,层次都可以,在遍历时,对每个结点的深度也使用栈(层次使用队列)进行存储。
int TreeHigh(BiTree T){
if(T == NULL) return 0;
SqStack S,H;
InitStack(S);
InitStack(H);
BiTNode *p = T;
int high = 0, treeHigh = 0;
while(p != NULL || !StackEmpty(S)){
if(p){
high++;
Push(S,p);
Push(H,high);
p = p->lchild;
}else{
Pop(S,p);
Pop(H,high);
treeHigh = max(high,treeHigh);
p = p->rchild;
}
}
return treeHigh;
}
思路二:层次遍历,可以进行优化,使用一个队列即可,记录下每一层的最后一个结点即可,(因为一层只需要一次high++即可)
int TreeHigh(BiTree T){
if(T == NULL) return 0;
LinkQueue Q;
InitQueue(Q);
BiTNode *p, *last = T, *nextLast; // last记录当前访问层的最后一个结点, nextLast记录下一层的最后一个结点。
EnQueue(Q,T);
int high = 0; // 高度
while(!QueueEmpty(Q)){
DeQueue(Q,p);
if(p->lchild != NULL){
nextLast = p->lchild;
EnQueue(Q,p->lchild);
}
if(p->rchild != NULL){
nextLast = p->rchild; // 每次入队时更新 nextLast
EnQueue(Q,p->rchild);
}
if(p == last){
high++;
last = nextLast; // 下一层
}
}
return high;
}
- 王道解法:使用顺序队列
int Btdepth(BiTree T){
if(!T)
return 0;
int front = 0, rear = 0; // rear指向尾结点下一个,front指向首结点
int last = 1, level = 0;
BiTree Q[MaxSize]; // 假设队列足够大
Q[rear++] = T; // 入队
BiTNode *p;
while(front < rear){ // front == rear时为空
p = Q[front++]; // 出队
if(p->lchild)
Q[rear++] = p->lchild;
if(p->rchild)
Q[rear++] = p->rchild;
if(front == last){ // 某一层遍历完了
level++;
last = rear;
}
}
return level;
}
6 设一棵二叉树中各结点的值互不相同,其先序遍历序列和中序遍历序列分别存于两个一维数组
A[1...n]
和B[1...n]
中,试编写算法建立该二叉树的二叉链表。
思路一:递归做法,每次根据根节点在A中的位置,找到根在中序遍历中的位置,由根节点分为左右子树,在去分别找左右子树的根,递归调用。
- 确定根在中序遍历中的位置
- 根据根在中序遍历的位置划分为左右子树,再分别找到左右子树的根,递归调用,回到步骤1
// left指定子树在B的起始位置,right指定末尾位置,root指定根,此处是在A中的下标
// 在B中的 left~right闭区间内,root为根结点[A下标],建立二叉树
void CreateTree(BiTree &T, int A[], int B[], int left, int root, int right, int n){
// if(left > right) return; 可不加 初次传参时要求 left <= right
T = (BiTNode*) malloc(sizeof(BiTNode));
T->value = A[root];
T->left = T-right = NULL;
int i,j,lroot,rroot; // lroot, rroot左右子树的根
for(i = 1; B[i] != A[root]; i++); // 找根在B数组中的位置
lroot = root + 1; // 如果有左子树的话,左子树必是 root + 1
if(i != left) // 有左子树
CreateTree(T->lchild, A, B, left, i - 1, lroot, n); // 构建左子树
rroot = root + (i - left) + 1; // root + 左子树结点个数 + 1 即为右子树根结点位置
if(i != right) // 有右子树
CreateTree(T->rchild, A, B, i + 1, right, rroot, n); // 构建右子树
}
CreateTree(T, A, B, 1, n, 1, n); // 函数调用
思路二:由栈考虑,先序遍历相当于入栈顺序,中序遍历相当于出栈顺序,入栈出栈顺序确定后,唯一确定一棵二叉树。
有如下对应关系。当扩展吧,不必掌握,,,自己想的
上一步操作 | 当前操作 | 处理过程 |
---|---|---|
无操作 | —— | 入栈,且该结点作为根节点 |
入栈 | 入栈 | 为上一步入栈结点的左子树 |
出栈 | 入栈 | 为上一步出栈结点的右子树 |
入栈 | 出栈 | 上一步入栈结点无左子树 |
出栈 | 出栈 | 上一步出栈结点无右子树 |
出栈 | —— | 上一步出栈结点为叶节点 |
- 未完成,很麻烦,半成品且有错
// op 记录上一步操作,-1 表示无操作, 0 表示入栈 1表示出栈
// stack,top 为栈的信息,记录入栈出栈的元素
// i,j记录访问 A,B的下标
void CreateTree(int A[], int B[],int i, int j, BiTNode* stack[], int top, int n, int op,BiTNode* node){ //先序和中序序列必须合法
if(i > n && j > n) return;
BiTNode *T, *p;
if(op == -1){
T = (BiTNode*)malloc(sizeof(BiTNode));
T->value = A[i];
T->lchild = T->rchild = NULL;
stack[++top] = T; // 入栈
// T->lchild = T->rchild = NULL;
op = 0;
i++;
}else{
T = stack[top];
// 当前操作为出栈
if(T->value == B[j]){
if(op == 0){
T->lchild = NULL;
node = T; // 保留一下,不然要被删除
}
else if(op == 1)
node->rchild = NULL;
top--;
op = 1;
j++;
}
// 当前操作为入栈
else{
if(op == 0)
p = T->lchild = (BiTNode*)malloc(sizeof(BiTNode));
else if(op == 1){
p = stack[++top] = node;
p->rchild = (BiTNode*)malloc(sizeof(BiTNode));
}
p->value = A[i];
p->lchild = p->rchild = NULL;
stack[++top] = p;
i++;
op = 0;
}
}
CreateTree(A,B,i,j,stack,top,n,op,node);
}
7 二叉树按二叉链表形式存储,试编写一个判别给定二叉树是否是完全二叉树的算法。
思路一:利用层次遍历,当遍历到某个结点发现只有一个左孩子或没有孩子时,那么剩下的所有结点都要求没有孩子结点。
设置一个 tag
标志位
- 当
tag == 0
时,出现rchild == NULL
时置tag = 1
, 同时,如果出现rchild != NULL && lchild == NULL
,即不是二叉树 - 当
tag == 1
时,出现lchild == NULL || rchild == NULL
时则判断不是二叉树。
bool IsCompleteBinaryTree(BiTree T){
if(!T)
return true;
Queue Q;
InitQueue(Q);
EnQueue(Q,T);
BiTNode *p;
int tag = 0;
while(!QueueEmpty(Q)){
DeQueue(Q,p);
if(p->lchild != NULL)
EnQueue(Q,p->lchild);
if(p->rchild != NULL)
EnQueue(Q,p->rchild);
if(tag && p->lchild != NULL && p->rchild != NULL)
return false;
if(tag == 0){
if(rchild == NULL)
tag = 1; // 第一个遍历到发现不是左右结点都存在的
else if(lchild == NULL) // 此时rchild != NULL
return false;
}
}
return true;
}
思路二(王道书):采用层次遍历,将所有结点加入队列(包括空结点)。遇到空结点时,查看其后是否有非空结点,若有,则二叉树不是完全二叉树。
bool IsComplete(BiTree T){
InitQueue(Q);
if(!T) return 1;
EnQueue(Q,T);
while(!IsEmpty(Q)){
DeQueue(Q,p);
if(p){
EnQueue(p->lchlid);
EnQueue(p->rchild);
}else{ // 结点为空,检查其后是否有非空结点
while(!IsEmpty(Q)){
DeQueue(Q,p);
if(p) // 结点非空,则二叉树为非完全二叉树
return 0;
}
}
}
}
8 假设二叉树采用二叉链表存储结构存储,试设计一个算法,计算一棵给定二叉树的所有双分支结点个数。
int DsonNodes(BiTree T){
if(T == NULL)
return 0;
if(T->lchild && T->rchild)
return DsonNodes(T->lchild) + DsonNodes(T->rchild) + 1;
else
return DsonNodes(T->lchild) + DsonNodes(T->rchild);
}
9 设树B是一棵采用链式结构存储的二叉树,编写一个把树B中所有结点的左、右子树进行交换的函数。
void SwapChildren(BiTree T){
if(!T) return;
SwapChildren(T->lchild); // 交换左子树
SwapChildren(T->rchild); // 交换右子树
BiTNode *t;
t = T->lchild; // 交换当前结点左右孩子
T->lchild = T->rchild;
T->rchild = t;
}
10 假设二叉树采用二叉链表存储结构存储,设计一个算法,求先序遍历序列中第 k( 1 <= k <= 二叉树结点个数)个结点的值。
思路一:使用递归的方法,去遍历左右子树,参数 k 使用的引用方式。
int Find_K(BiTree T,int &k){
if(k >= 0 && T){
k--;
if(k == 0)
return T->value;
else{
int v = -1;
v = Find_K(T->lchlid, k);
if(v != -1) return v;
v = Find_K(T->rchild, k);
return v;
}
}
return -1;
}
思路二:使用非递归的前序遍历,返回值指示查找结果,结点的值通过参数返回。
bool Find_K2(BiTree T, int k, int &res){
stack<BiTNode*> S;
BiTNode *p = T;
if(k <= 0)
return false;
while(p != NULL || !S.empty()){
if(p){
k--;
S.push(p);
if(k == 0){
res = p->value;
return true;
}
p = p->lchild;
}else{
p = S.top();
S.pop(); // p结点及p结点左子树都被访问过了
p = p->rchild;
}
}
return false;
}
思路三(王道):在全局设置了变量 i
,解决了思路一中需要使用引用从而改变了 k
的值,但是并没有对 k
越界及不合法进行处理。
int i = 1;
ElemType PreNode(BiTree b, int k){
if(b == NULL)
return '#';
if(i == k)
return b->value;
i++;
ch = PreNode(b->lchild,k);
if(ch != '#')
return ch;
ch = PreNode(b->rchild,k);
return ch;
}
11 已知二叉树以二叉链表存储,编写算法完成:对于树中每个元素值为x的结点,删除以它为根的子树,并释放相应的空间。
思路:采用先序遍历的方式,当发现当前结点值为 x 时,释放。
🐯 删除子树时应采用后序遍历的方式。
一定要注意删除子树后赋值为 NULL,free函数只是释放了空间,变量的值不会修改
void Release(BiTree &T){
if(!T) return ;
Release(T->lchild);
Release(T->rchild);
free(T);
}
void PreOrder(BiTree &T, int x){
if(!T) return;
if(T->value == x){
Release(T);
T = NULL; // 一定要修改为NULL,不然返回后 T 是不为空的,作为父结点的孩子结点在遍历时也会出现问题
}
if(T){
PreOrder(T->lchild,x);
PreOrder(T->rchild,x);
}
}
王道书代码采用层次遍历,感觉代码并不简洁 --> 如果考虑递归栈溢出问题,可以使用非递归或课本的层次遍历。
12 在二叉树中查找值为 x 的结点,试编写算法(用C语言)打印值为 x 的结点的所有祖先,假设值为 x 的结点不多于一个。
提示:本章节选择题第 5 题 --> 当后序遍历到值为 x 的结点时,栈顶为 x ,从栈底到栈顶即为根结点到x的路径,即x的所有祖先。
思路:非递归后序遍历二叉树,当遍历到值为 x 的结点时,停止遍历,输出栈中的内容。
void PostOrder(BiTree T){
Stack S;
InitStack(S);
BiTNode *p = T, *r = NULL;
while(p || !StackEmpty(S)){
if(p){
Push(S,p);
p = p->lchild;
}else{
GetTop(S,p);
if(p->value == x)
break;
if(p->rchild && p->rchild != r)
p = p->rchild;
else{
Pop(S,p);
visit(p);
r = p;
p = NULL;
}
}
}
while(!StackEmpty(S)){
Pop(S,p);
printf(p->value);
}
}
为什么后序遍历可以,而先序遍历,中序遍历不可以?
后序遍历时,当遍历到值为 x 的结点时,说明该结点左右子树已被访问过,而其祖先结点:1. 处于父结点的左子树时,其父结点的右子树未加入栈。 2. 处于父结点的右子树时,其父结点的左子树已经访问完并出栈。 可知,栈中只有其祖先结点。
先序遍历过程中,第一次访问即将该结点删除了,所以先序遍历无法保存路径的。 中序遍历过程中,第二次访问该结点时会被删除,所以如果 x 是某个子树的右孩子时,路径也无法被完整保存。只有其所有祖先结点和x均是左孩子时(一条向左下的线)中序遍历才可以。
13 设一颗二叉树的结点结构为(LLINK,INFO,RLINK),ROOT为指向该二叉树根结点的指针,p和q分别指向该二叉树中任意两个结点的指针,试编写算法ANCESTOR(ROOT,p,q,r),找到p和q的最近公共祖先结点r。
并没有想到解决方法,看的王道书。。。
BiTNode *ANCESTOR(BiTree ROOT, BiTNode *p, BiTNode *q, BiTNode *&r){
Stack S[], S1[]; // S存放 p 的公共祖先, S1 存放 q 的公共祖先,假设q在p右边
int top = -1, top1 = -1, int tag = 0;
BiTNode *t = ROOT, *m = NULL;
while(t || top >= 0){ // 后序遍历
if(t){
S[++top] = t;
t = t->lchild;
}else{
t = S[top];
if(t == p || t == q)
if(tag == 0){
for(int i = 0; i <= top; i++)
S1[++top1] = S[i];
tag = 1;
}else{
for(int i = top1; i >= 0; i--)
for(int j = top; j >= 0; j--){
if(S[top1] == S[top2]) // 找到最近的公共父结点
return S[top1];
}
}
if(t->rchild && t->rchild != m)
t = t->rchild;
else{
t = S[top--];
m = t;
t = NULL;
}
}
}
return NULL; // 未找到
}
14 假设二叉树采用二叉链表存储结构,设计一个算法,求非空二叉树b的宽度(即具有结点数最多的那一层的结点个数)。
思路一:使用层次遍历,记录每一层最后一个结点,用于计数
int BtWidth(BiTree T){
if(!T)
return 0;
int front = 0, rear = 0; // rear指向尾结点下一个,front指向首结点
int last = 1, maxWidth = 1;
BiTree Q[MaxSize]; // 假设队列足够大,此处可以使用循环队列哦!,因为front扫过的结点就不再使用了
Q[rear++] = T; // 入队
BiTNode *p;
while(front < rear){ // front == rear时为空
p = Q[front++]; // 出队
if(p->lchild)
Q[rear++] = p->lchild;
if(p->rchild)
Q[rear++] = p->rchild;
if(front == last){ // 某一层遍历完了
if(rear - front > maxWidth)
maxWidth = rear - front; // 下一层的结点个数比当前层多
last = rear;
}
}
return maxWidth;
}
思路二(王道思路):将所有结点在入队列时,将其所在层数也入到队列,在层次遍历后再去统计每一层的个数,较麻烦。且不能使用循环队列,因为要考虑所有结点所在层数的信息都要保留下来,最后进行计数。推荐思路一
15 设有一棵满二叉树(所有结点值均不相同),已知其先序序列为pre,设计一个算法求其后序post。
应使用递归方法:在遍历某棵树时,先序遍历的根节点(第一个遍历到的)作为后序遍历的最后一个结点。
(王道)
// l,r 对应pre的起始与末尾, ll,rr对应post的起始与末尾
void PreToPost(int pre[], int post[], int l, int r,int ll, int rr){
post[rr] = pre[l];
if(l == r)
return;
else{
int n = (r - l) / 2;
Post(pre,post,l + 1, l + n, ll, ll + n - 1);
Post(pre,post,l + n + 1, r, ll + n, rr - 1);
}
}
PreToPost(pre,post,1,n,1,n);
16 设计一个算法将二叉树的叶节点按从左到右的顺序连成一个单链表,表头指针为head。二叉树按二叉链表的方式存储,链接时用叶节点的右指针域来存放单链表指针。
思路:因为先序、中序、后序遍历都是从左到右遍历,所以可以使用三个遍历中的一种。
此处使用先序遍历的方式
- 当遍历到第一个叶结点时,令
r
和head
指向该叶节点 - 遍历到其他叶节点时,令
r->rchild = 该结点; r = 该结点;
BiTNode *PreOrder(BiTree t){
stack s;
InitStack(s);
BiTNode *p = t, *r = NULL, *head = NULL;
while(p || !IsEmpty(s)){
if(p){
Push(s,p);
p = p->lchild;
}else{
Pop(s,p);
if(p->lchild == NULL && p->rchild == NULL){
if(r == NULL){
r = p;
head = r;
}else{
r->rchild = p;
r = p;
}
}
p = p->rchild;
}
}
r->rchild = NULL;
return head;
}
递归做法
// 需要设置全局变量来使用递归
BiTNode *head, *r = NULL;
void PreOrder(BiTree t){
if(t){
if(t->lchild == NULL && t->rchild == NULL){
if(r == NULL)
head = r = t;
else{
r->rchild = t;
r = t;
}
}
PreOrder(t->lchild);
PreOrder(t->rchild);
r->rchild = NULL;
}
}
// 做法二
BiTNode *GetLeaves(BiTree t){
BiTNode *head;
PreOrder(t, head, NULL);
return head;
}
void PreOrder(BiTree t, BiTNode *&head, BiTNode *r){
if(t){
if(t->lchild == NULL && t->rchild == NULL){
if(r == NULL)
head = r = t;
else{
r->rchild = t;
r = t;
}
}
PreOrder(t->lchild,head,r);
PreOrder(t->rchild,head,r);
r->rchild = NULL;
}
}
17 试设计判断两棵二叉树是否相似的算法。所谓二叉树 T 1 T_1 T1 和 T 2 T_2 T2 相似,指的是 T 1 T_1 T1 和 T 2 T_2 T2 都是空的二叉树或都是只有一个根结点;或者 T 1 T_1 T1 的左子树和 T 2 T_2 T2 的左子树是相似的,且 T 1 T_1 T1 的右子树和 T 2 T_2 T2 的右子树是相似的。
思路:递归
if(T1孩子结点数 == T2孩子结点数)
if(结点数为0) return true;
if(结点数为1) 判断T1孩子与T2孩子是否相似,相似返回true,否则返回false
if(结点数为2) 判断T1左孩子与T2左孩子,T1右孩子与T2右孩子是否相似,如有一个不相似,返回false,否则返回true
else // 即结点数不同,返回false
个人代码很复杂,不如王道
bool Similar(BiTree T1, BiTree T2){
if((T1 == NULL && T2 != NULL) || (T1 != NULL && T2 == NULL))
return false; // 有一个为空,另一个不为空,必不相似
else if( (T1 == NULL && T2 == NULL) || (T1->lchild == NULL && T1->rchild == NULL) && (T2->lchild == NULL && T2->rchild == NULL))
return true; // 都为空二叉树或都是只有一个根结点
else
return Similar(T1->lchild,T2->lchild) && Similar(T1->rchild,T2->rchild); // 判断孩子们是否都相似
}
王道
bool Similar(BiTree T1, BiTree T2){
if(T1 == NULL && T2 == NULL) // 都为空
return true;
else if(T1 != NULL || T2 != NULL) // 一个为空,另一个不为空
return false;
else
return Similar(T1->lchild,T2->lchild) && Similar(T1->rchild,T2->rchild); // 也包含判断只有一个根结点的情况。
}
18 写出在中序线索二叉树里查找指定结点在后序的前驱结点的算法。
后序遍历中某个结点的前驱:
- 结点有右孩子 --> 右孩子 --> 返回该结点右孩子
- 结点无右孩子有左孩子 --> 左孩子结点 --> 返回该结点左孩子
- 结点无孩子 --> 祖先结点中第一个有左孩子的结点的左孩子 --> 找中序遍历下该结点的前驱,即父结点,若父结点无左孩子,则接着往上找
- 上述三种都没有符合条件的,则无前驱。
BiTNode *FindPre(BiTree t, BiTNode *p){
BiTNode *r;
if(p->rtag == 0)
return p->rchild;
else if(p->ltag == 0)
return p->lchild;
else{
r = p;
while(r && r->ltag == 1)
r = r->lchild;
// 如果有左孩子则返回的左孩子即 r->ltag == 0
if(r->ltag == 0)
return r->lchild;
else
return NULL; //所有祖先都无左孩子, r此时一定为NULL
}
}
5.4
4 编程求以孩子兄弟表示法存储的森林的叶节点数。
思路:计算森林中的叶节点数,实际就是计算 firstchild == NULL
的结点的个数。
int Leaves(CSTree t){
int num = 0;
if(t){
if(t->firstchild == NULL)
num++;
else
num += Leaves(t->firstchild);
num += Leaves(t->nextsibling);
}
return num;
}
// 简洁版
int Leaves(CSTree t){
if(t == NULL)
return 0;
if(t->firstchild)
return Leaves(t->firstchild) + Leaves(t->nextsibling);
else
return 1 + Leaves(t->nextsibling);
}
5 以孩子兄弟链表为存储结构,请设计递归算法求树的深度
思路:根结点深度为1
当某个结点作为第一个孩子结点时,其深度是父结点的深度加1;当作为兄弟结点时,深度不变,为父结点的深度。
typedef struct node{
Elemtype data;
struct node *fch, *nsib;
}*Tree;
int TreeDepth(Tree t){
int fd,nd;
if(t == NULL)
return 0;
else{
fd = TreeDepth(t->fch);
nd = TreeDepth(t->nsib);
return fd + 1 > nd ? fd + 1 : nd;
}
}
6 已知一棵树的层次序列及每个结点的度,试编写算法构造此树的孩子-兄弟结点。
- 如果结点的度 >= 1,那么必有左孩子结点。
- 如果结点的度 > 1,那么该结点的左孩子结点
p
必有右孩子结点q
(q
是否还有右孩子取决于度的大小) - 如果结点的度为 0,那么该结点无左孩子
typedef struct node{
Elemtype data;
struct node *fch, *nsib;
}*Tree;
Tree CreateTree(Tree &t, Elemtype order[], int degree[], int n){
node *pointer = new node[n];
// pointer对各个树的结点进行建立
for(int i = 0; i < n; i++){
pointer[i].data = order[i];
pointer[i].fch = pointer[i].nsib = NULL;
}
int k = 0;
for(int i = 0; i < n; i++){
if(degree[i]){ // 结点度数大于0
k++; // k 为子女结点的序号
pointer[i].fch = pointer[k]; // 建立 i 与子女 k 的链接
for(int j = 2; j <= degree[i]; j++){
k++;
pointer[k - 1].nsib = pointer[k]; // 将层次遍历时右兄弟结点加入
}
}
}
t = pointer[0];
delete pointer[];
}
🐯 并没有想到如何实现,没有理清层次遍历的关系,也没有想到使用 将所有的结点先建立好,再依次连接 的方式。
!!! 该代码思路是使用 双指针 的方式,i
指向根结点,k
指向 i
的子女结点。
集合
并查集
并查集实现集合元素之间的并、查。
结构体定义
#define SIZE 13
int UFSets[SIZE]; // 数组
常用操作
-
初始化
void Initial(int S[]){ for(int i = 0; i < SIZE; i++) S[i] = -1; // -1 指代为根结点,初始时,所以结点各自为一个集合 }
-
查找
int Find(int S[], int x){ // x是在数组的下标 while(S[x] >= 0) x = S[x]; return x; // S[x] = -1 时,即找到了根 }
-
合并
void Union(int S[], int Root1, int Root2){ // Root1, Root2是两个不同集合的根,如果给定两个元素,要合并所在的两个集合,要先Find到根,再去合并 if(Root1 == Root2) return ; S[Root2] = Root1; // 写 S[Root1] = Root2也可以 }
🐯 时间复杂度
-
Find
O ( n ) O(n) O(n) -
Union
O ( 1 ) O(1) O(1)
若给定是两个集合元素时,将两个元素所在集合合并,需要找到根 O ( n ) O(n) O(n)若要对 n n n 个独立元素通过多次
Union
合并为一个集合 O ( n 2 ) O(n^2) O(n2)
优化一:对Union优化
优化思路: 在每次Union操作构建树的时候,尽可能让树不长高高
优化方法:
- 用根节点的绝对值表示树的结点总数
- Union操作,让小树合并到大数
void Union(int S[], int Root1, int Root2){
if(Root1 == Root2)
return ;
if(S[Root2] > S[Root1]){
S[Root1] += S[Root2]; // 累加结点数
S[Root2] = Root1; // 合并
}else{
S[Root2] += S[Root1];
S[Root1] = Root2;
}
}
🐯 时间复杂度
-
Find
O ( l o g 2 n ) O(log_2n) O(log2n) -
Union
O ( 1 ) O(1) O(1)
若给定是两个集合元素时,将两个元素所在集合合并,需要找到根 O ( l o g 2 n ) O(log_2n) O(log2n)若要对 n n n 个独立元素通过多次
Union
合并为一个集合 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
时间复杂度证明:
优化二:对Find优化
优化思路: 压缩路径,先找到根结点,再将查找路径上所有结点都挂到根结点下
优化方法:
int Find(int S[], int x){
int root = x;
while(S[root] >= 0)
root = S[root];
while(x != root){ // 类似于树的路径
int t = S[x];
S[x] = root;
x = t;
}
}
🐯 时间复杂度
-
Find
O ( α ( n ) ) O(\alpha (n)) O(α(n)) --> α ( n ) \alpha (n) α(n) 是一个增长很缓慢的函数,对于常见的 n 值,通常 α ( n ) ≤ 4 \alpha(n) \leq 4 α(n)≤4 -
Union
O ( 1 ) O(1) O(1)
若给定是两个集合元素时,将两个元素所在集合合并,需要找到根 O ( α ( n ) ) O(\alpha (n)) O(α(n))若要对 n n n 个独立元素通过多次
Union
合并为一个集合 O ( n α ( n ) ) O(n\alpha (n)) O(nα(n))
总结
方案二
🍏 之前自学的并查集,感觉代码更好
🐯 初始化
采用数组代表整个森林,初始时每个森林的树根为自己
#define maxn 200
// fa存储每个元素的父结点,初始化父结点设为自己
int fa[maxn+1];
void init()
{
for(int i = 0;i <= maxn;i++)
fa[i] = i;
}
🐯 查询
一般用递归法实现对代表元素的查询:递归访问父节点,直至根节点(根节点的标志就是父节点是本身)。
根节点相同的两个元素属于同一个集合,上面也说到了。所以判断A,B是否属于个集合直接判断find(A)和d(B)是否相同即可。
int find(int x)
{
if(fa[x] == x)
return x;
else
return find(fa[x]);
}
路径压缩:对元素所属的集合进行记忆化存储,不用每次都要去向前查找
我们只关心某个元素的根节点是谁,而不需要每次去向上查询
int find(int x)
{
if(fa[x] == x)
return x;
else
{
fa[x] = find(fa[x]);
// 父结点设为根节点
return fa[x];
// 返回父结点
}
}
// 简化
int find(int x)
{
return fa[x] == x ? x : (fa[x] = find(fa[x]));
}
修改前 修改后
🐯 集合合并
找到两个集合的代表元素,并将前者的父结点设置为后者,或者反过来也可以
void merge(int i,int j)
{
fa[find(i)] = find(j);
}
🐯 最终代码
#define maxn 200
int fa[maxn+1];
void init(){
for(int i = 0;i <= maxn;i++)
fa[i] = i;
}
int find(int x){
return fa[x] == x ? x : (fa[x] = find(fa[x]));
}
void merge(int i,int j){
fa[find(i)] = find(j);
}
图
图的存储
邻接矩阵法
结构体定义
#define MaxVertexNum 100
typedef char VertexType; // 顶点数据类型
typedef int EdgeType; // 边数据类型
typedef struct{
VertexType Vex[MaxVertxNum]; // 顶点表
EdgeType Edge[MaxVertxNum][MaxVertxNum];// 边表
int vexnum, arcnum; // 顶点数,弧数
}MGraph;
- 普通图
边表使用 0 0 0 、 1 1 1 表示边是否存在,此时可以采用 b o o l bool bool 类型或值为 0 0 0 和 1 1 1 的枚举类型
无向图必为对称矩阵。 A [ i ] [ j ] = A [ j ] [ i ] A[i][j] = A[j][i] A[i][j]=A[j][i] 可以采用压缩存储- 带权图
#define INFINITY (1<<31) - 1
-->int
最大值 --> 计组第二章
当用邻接矩阵存储带权图时, 0 0 0 或 ∞ \infty ∞ 来表示边不存在, ∞ \infty ∞ 可用如上定义。
注: 可以同时使用 0 0 0 和 ∞ \infty ∞ 来表示边不存在。
邻接矩阵的表示是唯一的,因为图中边的信息在矩阵中有确定的位置。
邻接表法
结构体定义
#define MaxVertexNum 100
typedef struct ArcNode{ // 边表结点
int adjvex; // 该弧指向的顶点位置
struct ArcNode *next; // 指向下一条弧的指针
// InfoType info; // 网的权值
}ArcNode;
typedef struct VNode{ // 顶点表结点
VertexType data; // 顶点信息
ArcNode *first; // 指向第一条依附该顶点的弧的指针
}VNode, AdjList[MaxVertexNum];
typedef struct{
AdjList vertices; // 邻接表
int vexnum,arcnum; // 图的顶点数和弧数
}ALGraph;
邻接表表示不唯一,因为邻接表的建立取决于读入边的顺序和边表中的插入算法。
简单对比
十字链表法
十字链表法是 有向图 的一种链式存储结构。
弧结点:
tailvex | headvex | hlink | tlink | (info) |
---|
tailvex
: 弧尾的编号
headvex
: 弧头的编号
hlink
: 指向弧头相同的下一个弧结点
tlink
: 指向弧尾相同的下一个弧结点
info
: 该弧的相关信息
顶点结点:
data | firstin | firstout |
---|
data
: 存放该顶点的数据信息
firstin
: 指向以该顶点为弧头的第一个弧结点
firstout
: 指向以该顶点为弧尾的第一个弧结点
⚠️ 提示
- 弧头相同的弧就在同一个链表上,弧尾相同的弧也在同一个链表上。出度、入度、出边、入边都好找了。
解决了邻接表不好找入度、入边的问题和邻接矩阵空间复杂度高的问题。- 图的十字链表表示不唯一,一个十字链表唯一确定一张图。
邻接多重表
邻接多重表是 无向图 的一种链式存储结构。
边结点:
ivex | ilink | jvex | jlink | (info) |
---|
ivex
: 该边依附的顶点编号
jvex
: 该边依附的顶点编号
ilink
: 指向下一条依附于顶点 ivex
的边
jlink
: 指向下一条依附于顶点 jvex
的边
info
: 该边的相关信息
顶点结点:
data | firstedge |
---|
data
: 存放该顶点的数据信息
firstedge
: 指向第一条依附于该顶点的边
⚠️ 提示
- 每个边只存储一份信息,解决了邻接表中每条边对应两份冗余信息、删除顶点、删除边等操作时间复杂度高的问题(在此处删除只需要修改指针即可)。 同时又避免使用邻接矩阵空间复杂度高的问题
- 所有依附于同一顶点的边串联在同一链表中,由于每条边依附于两个顶点,因此每个边结点同时链接在两个链表中。
简单对比
图的基本操作
-
Adjacent(G,x,y)
: 判断图G是否存在边 < x , y > <x,y> <x,y>或 ( x , y ) (x,y) (x,y)。无向图、邻接矩阵 无向图、邻接表 有向图、邻接矩阵 有向图、邻接表 操作 return Edge[x][y];
遍历 x
点的链表,如果找到y
返回true
,否则返回false
return Edge[x][y];
遍历 x
点的链表,如果找到y
返回true
,否则返回false
时间复杂度 O ( 1 ) O(1) O(1) $O(1)-O( v )$ -
Neighbors(G,x)
: 列出图G中与结点x相接的边无向图、邻接矩阵 无向图、邻接表 有向图、邻接矩阵 有向图、邻接表 操作 遍历 x
点所在行,如果值为1
,则该边与x
邻接遍历 x
点的链表遍历 x
点所在行,如果值为1
,则该边与x
邻接出边:遍历 x
点的链表
入边:遍历整个表,找所有下标为x
的边时间复杂度 $O( v )$ $O(1)-O( -
InsertVertex(G,x)
: 在图G中插入顶点x无向图、邻接矩阵 无向图、邻接表 有向图、邻接矩阵 有向图、邻接表 操作 在顶点列表中存入值 Vex[x] = value;
, 邻接矩阵中A[x][~],A[~][x] = 0;
, 顶点数vexnum++;
在数组位置中插入该顶点,并赋值,同时 first
指针指向空。vertices[x].data = value; vertices[x].first = NULL; vexnum++;
同无向图 同无向图 时间复杂度 O ( 1 ) O(1) O(1), 这里和初始化与删除的方式有关,假设初始化是邻接矩阵大小固定,且全部初始化为0,如果是 O ( 1 ) O(1) O(1),则在删除顶点时,将该顶点所在行和列重置为0,删除操作时间复杂度为$O( v ) 。如果每次删除后顶点所在行和列不重置,则删除操作时间复杂度为 。如果每次删除后顶点所在行和列不重置,则删除操作时间复杂度为 。如果每次删除后顶点所在行和列不重置,则删除操作时间复杂度为O( 1 -
DeleteVertex(G,x)
: 从图G中删除顶点x无向图、邻接矩阵 无向图、邻接表 有向图、邻接矩阵 有向图、邻接表 操作 遍历 x
点所在行和列,全部重置为0,并将顶点所在位置标记为空顶点。注: 可以使用一个bool
类型的数组进行标记,那么在插入顶点时也可以利用该数组进行插入位置的选择。遍历 x
点的链表,依次删除。
遍历整个邻接表,将所有边指向x
的边结点删除。同无向图 出边:遍历 x
点的链表,依次删除
入边:遍历整个表,找所有下标为x
的边并删除时间复杂度 $O( v )$ $O(1)-O( -
AddEdge(G,x,y)
: 若无向边 ( x , y ) (x,y) (x,y) 或有向边 < x , y > <x,y> <x,y> 不存在,则向图G中添加该边无向图、邻接矩阵 无向图、邻接表 有向图、邻接矩阵 有向图、邻接表 操wu作 先调用 Adjacent(G,x,y)
判断是否存在,若存在,则退出,若不存在,Edge[x][y] = Edge[y][x] = 1; arcnum++;
法一:遍历 x
点的链表至尾部,插入y
;遍历y
点的链表至尾部,插入x
法二:将y
插入x
点链表的头部;将x
插入y
点链表的头部先调用 Adjacent(G,x,y)
判断是否存在,若存在,则退出,若不存在,Edge[x][y] = 1; arcnum++;
法一:遍历 x
点的链表至尾部,插入y
;
法二:将y
插入x
点链表的头部;时间复杂度 O ( 1 ) O(1) O(1) 头插: O ( 1 ) O(1) O(1)
尾插:$O(v )$ -
RemoveEdge(G,x,y)
: 若无向边 ( x , y ) (x,y) (x,y) 或有向边 < x , y > <x,y> <x,y> 存在,则从图G中删除该边无向图、邻接矩阵 无向图、邻接表 有向图、邻接矩阵 有向图、邻接表 操wu作 先调用 Adjacent(G,x,y)
判断是否存在,若不存在,则退出,若存在,Edge[x][y] = Edge[y][x] = 0; arcnum--;
遍历 x
点的链表找到y
并删除;遍历y
点的链表找到x
并删除先调用 Adjacent(G,x,y)
判断是否存在,若不存在,则退出,若存在,Edge[x][y] = 0; arcnum--;
遍历 x
点的链表找到y
并删除;时间复杂度 O ( 1 ) O(1) O(1) $O(1)-O( v )$ -
FirstNeighbor(G,x)
: 求图G中顶点x
的第一个邻接点,若有则返回顶点号。若x
没有邻接点或图中不存在x
,则返回-1
。// 邻接矩阵 适用于无向图、有向图、带权图 int FirstNeighbor(MGraph G, int x){ if(x != -1){ // 加入 -1 判断的好处:通过该函数和NextNeighbor()时,如果未找到结点,则返回-1,代表不存在,如此,在相互调用时不会出现下标错误。可以使用该方法进行某个顶点边的遍历。 for(int col = 0; col < G.vexnum; col++){ if(G.Edge[x][col] > 0 && G.Edge[x][col] < INFINITY) return col; // 第一个存在的边 } } return -1; // 不存在 } // 邻接表 适用于无向图、有向图、带权图 int FirstNeighbor(ALGraph G, int x){ if(x != -1){ ArcNode *p = G.vertices[x].first; if(p != NULL) return p->adjvex; // 第一个存在的边 } return -1; // 不存在 }
邻接矩阵
无向图 O ( 1 ) − O ( ∣ v ∣ ) O(1)-O(|v|) O(1)−O(∣v∣)
有向图 O ( 1 ) − O ( ∣ v ∣ ) O(1)-O(|v|) O(1)−O(∣v∣)邻接表
无向图 O ( 1 ) O(1) O(1) 有向图 找出边: O ( 1 ) O(1) O(1) 找入边: O ( 1 ) − O ( ∣ E ∣ ) O(1)-O(|E|) O(1)−O(∣E∣)
-
NextNeighbor(G,x,y)
: 假设图G中顶点y
是顶点x
的下一个邻接点,返回除y
之外顶点x
的下一个邻接点的顶点号,若y
是x
的最后一个邻接点,则返回-1
// 邻接矩阵 适用于无向图、有向图、带权图 int NextNeighbor(MGraph G, int x, int y){ if(x != -1 && y != -1){ for(int col = y + 1; col < G.vexnum; col++){ // y + 1 的位置开始查找 if(G.Edge[x][col] > 0 && G.Edge[x][col] < INFINITY) return col; } } return -1; // 不存在 } // 邻接表 适用于无向图、有向图、带权图 int NextNeighbor(ALGraph G,int x, int y){ if(x != -1 && y != -1){ ArcNode *p = G.vertices[x].first; while(p != NULL && p->adjvex != y) // 找到y p = p->next; if(p != NULL && p->next != NULL) // 如果y的下一个邻接顶点存在,则返回 return p->next->adjvex; } return -1; // 不存在 }
邻接矩阵
无向图 O ( 1 ) − O ( ∣ v ∣ ) O(1)-O(|v|) O(1)−O(∣v∣)
有向图 O ( 1 ) − O ( ∣ v ∣ ) O(1)-O(|v|) O(1)−O(∣v∣)邻接表
无向图 O ( 1 ) − O ( ∣ v ∣ ) O(1)-O(|v|) O(1)−O(∣v∣) 咸鱼写的 O ( 1 ) O(1) O(1),感觉不对,所以跟着感觉走。 有向图 找出边: O ( 1 ) − O ( ∣ v ∣ ) O(1)-O(|v|) O(1)−O(∣v∣) 找入边: O ( 1 ) − O ( ∣ E ∣ ) O(1)-O(|E|) O(1)−O(∣E∣)
-
Get_edge_value(G,x,y)
: 获取图G中边 ( x , y ) (x,y) (x,y) 或 < x , y > <x,y> <x,y> 对应的权值。 -
Set_edge_value(G,x,y,v)
: 设置图G中边 ( x , y ) (x,y) (x,y) 或 < x , y > <x,y> <x,y> 对应的权值为v。
9和10操作大同小异,关键是找到该边,一个赋值,一个返回而已。查找操作在
Adjacent()
上稍微修改一下即可。邻接矩阵
无向图 O ( 1 ) O(1) O(1)
有向图 O ( 1 ) O(1) O(1)邻接表
无向图 O ( 1 ) − O ( ∣ v ∣ ) O(1)-O(|v|) O(1)−O(∣v∣) 有向图 O ( 1 ) − O ( ∣ v ∣ ) O(1)-O(|v|) O(1)−O(∣v∣)
图的遍历
广度优先遍历BFS
类似于树的层次遍历
bool visited[MAX_VERTEX_NUM]; // 访问标记数组
void BFSTraverse(Graph G){
for(int i = 0; i < G.vexnum; ++i)
visited[i] = FALSE; // 访问标记数组初始化
InitQueue(Q);
for(int i = 0; i < G.vexnum; ++i)
if(!visited[i]) // 对每个连通分量调用一次BFS
BFS(G,i);
}
void BFS(Graph G, int v){
visit(v); // 访问初始顶点v
visited[v] = TRUE;
Enqueue(Q,v);
while(!isEmpty(Q)){
DeQueue(Q,v);
for(int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w))
// 访问 v 的所有邻接结点
if(!visited[w]){ // 未访问过
visit[w];
visited[w] = TRUE;
EnQueue(Q,w); // 入队
}
}
}
🐯 注意
- 上述算法中调用
BFS
的次数等于连通分量数 - 同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一,基于邻接矩阵生成的广度优先生成树唯一
- 同一个图邻接表表示方式唯一,因此广度优先遍历序列不唯一,基于邻接表生成的广度优先生成树也不唯一
空间复杂度:最坏情况,辅助队列大小为 O ( ∣ v ∣ ) O(|v|) O(∣v∣) ,即所有顶点都围绕在起始点周围。
时间复杂度:
邻接矩阵
访问 ∣ v ∣ |v| ∣v∣ 个顶点需要 O ( ∣ v ∣ ) O(|v|) O(∣v∣) 的时间
查找每个顶点的邻接点都需要 O ( ∣ v ∣ ) O(|v|) O(∣v∣) 的时间,总共 ∣ v ∣ |v| ∣v∣ 个顶点
时间复杂度 = O ( ∣ v ∣ 2 ) O(|v|^2) O(∣v∣2)邻接图
访问 ∣ v ∣ |v| ∣v∣ 个顶点需要 O ( ∣ v ∣ ) O(|v|) O(∣v∣) 的时间查找各个顶点的邻接点共需要 O ( ∣ E ∣ ) O(|E|) O(∣E∣) 的时间,
时间复杂度 = O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
深度优先遍历DFS
类似于树的先根遍历
bool visited[MAX_VERTEX_NUM]; // 访问标记数组
void DFSTraverse(Graph G){
for(int i = 0; i < G.vexnum; ++i)
visited[i] = FALSE; // 访问标记数组初始化
for(int i = 0; i < G.vexnum; ++i)
if(!visited[i]) // 对每个连通分量调用一次DFS
DFS(G,i);
}
void DFS(Graph G, int v){
visit(v); // 访问初始顶点v
visited[v] = TRUE;
for(int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w))
// 访问 v 的所有邻接结点
if(!visited[w]){ // 未访问过
DFS(G, w);
}
}
🐯 注意事项同BFS
空间复杂度:来自函数调用栈,最坏情况,递归深度为 O ( ∣ v ∣ ) O(|v|) O(∣v∣),最好情况, O ( 1 ) O(1) O(1)
时间复杂度:
邻接矩阵
访问 ∣ v ∣ |v| ∣v∣ 个顶点需要 O ( ∣ v ∣ ) O(|v|) O(∣v∣) 的时间
查找每个顶点的邻接点都需要 O ( ∣ v ∣ ) O(|v|) O(∣v∣) 的时间,总共 ∣ v ∣ |v| ∣v∣ 个顶点
时间复杂度 = O ( ∣ v ∣ 2 ) O(|v|^2) O(∣v∣2)邻接图
访问 ∣ v ∣ |v| ∣v∣ 个顶点需要 O ( ∣ v ∣ ) O(|v|) O(∣v∣) 的时间查找各个顶点的邻接点共需要 O ( ∣ E ∣ ) O(|E|) O(∣E∣) 的时间,
时间复杂度 = O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
最小生成树
prim算法
从某一个顶点开始构建生成树;每次将最小代价的新顶点纳入生成树,直到所有的顶点都纳入为止。
kruskal算法
最短路径
BFS求单源最短路径
适用于无权图
bool visited[MAX_VERTEX_NUM]; // 访问标记数组
bool d[MAX_VERTEX_NUM];
void BFS(Graph G, int v){
for(int i = 0; i < G.vexnum; ++i)
d[i] = INFINITY;
d[v] = 0;
visited[v] = TRUE;
Enqueue(Q,v);
while(!isEmpty(Q)){
DeQueue(Q,v);
for(int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w))
// 访问 v 的所有邻接结点
if(!visited[w]){ // 未访问过
visited[w] = TRUE;
d[w] = d[v] + 1; // 路径长度加 1
EnQueue(Q,w); // 入队
}
}
}
使用邻接矩阵的时间复杂度为 O ( ∣ v ∣ 2 ) O(|v|^2) O(∣v∣2)
使用邻接表的时间复杂度为 O ( ∣ v ∣ + ∣ E ∣ ) O(|v| + |E|) O(∣v∣+∣E∣)
Dijkstra算法
贪心思想
循环遍历所有顶点,找到还没确定最短路径,且 dist
最小的顶点 Vi
,令 final[i] = true
。并检查所有邻接自 Vi
的顶点,对于邻接自 Vi
的顶点 Vj
,若 final[j] == false && dist[i] + arcs[i][j] < dist[j]
则令 dist[j] = dist[i] = arcs[i][j]; path[j] = i;
// N 指代顶点个数
//邻接矩阵
int arcs[N][N];
// 到每个顶点的最短路
int dist[N];
// 是否求出最短路
bool final[N];
// 路径,记录前驱结点
int path[N];
// dijkstra
void dijkstra(int v)
{
for(int i = 0; i < N; i++){
dist[k] = arcs[0][i];
path[k] = (arcs[0][i] == INFINITY ? -1 : 0);
}
dist[v] = 0;
path[v] = -1;
// 共 N-1 轮处理
for (int i = 0; i < N - 1; i ++ )
{
// 找 dist 数组中距离最小且未被访问过的顶点
int t = -1;
for (int j = 1; j < N; j ++ )
if (!final[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 更新 dist 数组
for (int j = 1; j < N; j ++ )
if(dist[t] + arcs[t][j] < dist[j]){
dist[j] = arcs[t][j] + dist[t];
path[j] = t;
}
final[t] = true;
}
}
void print_path(int t){
if(t != -1)
print_path(path[t]);
print("%d ", t);
}
时间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
优化拓展:可以使用优先队列来存储源点到各顶点的距离,此处不再展开。
Floyd算法
动态规划思想
void floyd(int n){
// 循环 k 轮,以 Vk作为中转点
for(int k = 0; k < n; k++) // --> 第 k 轮后,A矩阵是指允许 1,2...,k作为中转点后的最短路径
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++){
if(A[i][j] > A[i][k] + A[k][j]){
A[i][j] = A[i][k] + A[k][j];
path[i][j] = k;
}
}
}
时间复杂度 O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3)
空间复杂度 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
算法设计
6.2
4 写出从图的邻接表表示转换成邻接矩阵表示的算法
MGraph Convert(ALGraph G){
MGraph M;
ArcNode *p;
M.vexnum = G.vexnum;
M.arcnum = G.vexnum;
for(int i = 0; i < G.vexnum; i++){ // 顶点信息复制
M.Vex[i] = G.vertices[i].data;
}
for(int i = 0; i < G.vexnum; i++){ // 边信息转换
p = G.vertices[i].first;
while(p != NULL){
Edge[i][p.adjvex] = 1;
p = p->next;
}
}
return M;
}
在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等均可省略)
设图的顶点分别存储在数组 v[n]
中。首先初始化邻接矩阵。遍历邻接表,在依次遍历顶点的边链表时,修改邻接矩阵的第 i
行的元素值。若链表边结点的值为 j
则设置 arcs[i][j] = 1;
。遍历完邻接表时,整个转换过程结束。此算法对无向图、有向图均适用。
// 王道写法
void Convert(ALGraph &G, int arcs[M][N]){
for(int i = 0; i < n; i++){ // 依次遍历各顶点表结点为头的边链表
p = (G->v[i]).firstart; // 取出顶点 i 的第一条出边
while(p != NULL){ // 遍历边链表
arcs[i][p->adjvex] = 1;
p = p->nextarc; // 取出下一条边
}
}
}