文章目录
前言
《数据结构 C语言版 | 第2版》
• 二叉树(P111)
• 递归(P61)
• 快速排序(P243)、归并排序(P254)
• DFS(深度优先搜索 P161)、BFS(广度优先搜索 P163)
一、二叉树
树结构是一类重要的非线性数据结构,直观来看,是以分支关系定义的层次结构。
树的结构定义是一个递归的定义,即在树的定义中又用到了树的定义,它道出了树的固有特性。
1.树的定义
树(Tree)是 n(n>=0)个结点的有限集,它或为空树(n = 0);或为非空树,对于非空树 T :
- 有且只有一个称之为根的结点;
- 除根结点以外的其余结点可分为 m(m>0)个互不相交的有限集T1, T2, … ,Tm,其中每一个集合本身又是一棵树,并且成为根的子根(Sub Tree)。
2.树的基本术语
(1)结点:树中的一个独立单元,包含一个数据元素及若干指向其子树的分支。
(2)结点的度:结点拥有的子树数。
(3)树的度:树内各节点度的最大值。
(4)叶子:度为 0 的结点成为叶子或者终端节点。
(5)层次:结点的层次从根开始定义,根为第一层,根的孩子为第二层。树中任意结点的层次等于其双亲结点的层次加 1。
(6)树的深度:树中结点的最大层次称为树的深度或高度。
(7)森林:是 m(m>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。因此也可以森林和树相互递归的定义来描述树。当 m != 0 时,在树根和其子树森林之间存在下列关系:
RF = { < root, ri > | i = 1, 2, 4, m, m > 0 }
3.二叉树的定义
其实除了定义外,我们还需要掌握其抽象数据类型定义、性质、存储结构、遍历和线索的知识,关于这些都可以在课本上继续学习的。
二叉树(Binary Tree)是 n (n>=0)个结点所构成的集合,它或为空树 (n=0) ;或为非空树,对非空树 T:
- 有且只有一个称之为根的结点;
- 除根结点以外的其余结点分为两个互不相交的子集 T1 和 T2 ,分别成为 T 的左子树和右子树,且 T1 和 T2 本身又都是二叉树。
二叉树与树的区别:
- 二叉树每个结点至多只有两颗子树(即二叉树中不存在度大于 2 的结点);
- 二叉树的子树有左右之分,其次序不能任意颠倒。
二叉树的 5 种基本形态:
- 空二叉树
- 仅有根结点的二叉树
- 右子树为空的二叉树
- 左子树为空的二叉树
- 左、右子树均非空的二叉树
二、递归
所谓递归是指,若在一个函数,过程或者数据结构定义的内部有直接(或间接)出现定义本身的应用,则称它们是递归的,或者是递归定义的。
1.关于递归
(1)什么是递归?函数调用本身。(即函数自己调用自己)
(2)构成递归的条件:
① 能将一个问题转换成一个更小的问题。并且新问题与原问题的解法相同。
② 必须有递归出口(否则会造成死递归)
(3)递归函数的底层:是由栈实现的,是系统帮我们写好的,我们可以直接使用。
(4)很多递推题都可以用递归解决,但耗时长,容易TL或爆栈。所以能用递推写的尽量都用递推写。(下面的例题1、2也可以递推写)
(5)递归 = 递推 + 回溯
例题1:Fibonacci 数列
例题2:求一个数的逆序(12345 --> 54321)
void f(int n)
{
if(n == 0) return; //递归出口
cout << n % 10;
f(n / 10); //转换为更小的问题
}
2.常用递归的三种情况
(1)定义是递归的
- 阶乘函数:Fact(n) = 1 (n=0) 或 n * Fact(n-1) (n>0)
long Fact(long n)
{
if(n == 0)
return 1; //递归终止的条件
else
return n * Fact(n-1); //递归步骤
}
- 二阶 Fibonacci 数列:if(n == 1 || n == 2) Fib(n) = 1; else Fib(n) = Fib(n-1) + Fib(n-2)
long Fib(long n)
{
if(n == 1 || n == 2)
return 1; //递归终止的条件
else
return Fib(n-1) + Fib(n-2); //递归步骤
}
(2)数据结构是递归的
算法3.9:遍历输出链表中各个结点的递归算法
(3)问题的解法是递归的
n 阶 Hanoi 塔问题
- 准备工作
为了便于描述算法,将搬动操作定义为 move(A, n, C),是指将编号为 n 的圆盘从 A 移到 C,同时设一个初值为 0 的全局变量 m,对搬动进行计数
int m = 0;
void move(char A, int n, char C)
{ cout << ++m << "," << n << ',' << C << endl;}
- 递归
(1)如果 n = 1,则直接将编号为 1 的圆盘从 A 移到 C,递归结束。
(2)否则:
- 递归,将 A 上编号为 1 至 n-1 的圆盘移到 B,C 做辅助塔。
- 直接将编号为 n 的圆盘从 A 移到 C;
- 递归,将 B 上编号为 1 至 n-1 的圆盘移到 C,A 做辅助塔。
void Hanoi(int n, char A, char B, char C)
{
if(n == 1)
move(A, 1, C); //将编号为 1 的圆盘从 A 移到 C
else
{
Hanoi(n-1, A, C, B); //将 A 上编号为 1 至 n-1 的圆盘移到 B,C 做辅助塔
move(A, n, C); //将编号为 n 的圆盘从 A 移到 C;
Hanoi(n-1, B, A, C); //将 B 上编号为 1 至 n-1 的圆盘移到 C,A 做辅助塔
}
}
【代码实现】
#include<iostream>
using namespace std;
int m;
void move(char A,int n,char C) //将编号为n的圆盘从A移到C
{
cout <<"次数"<<++m<<":"<<n<<","<<A<<"-->"<<C<<endl;
}
void han(int n,char A,char B,char C) //将A的n个圆盘通过B移动到C
{
if(n==1) move(A,1,C); //递归出口
else //转换为更小的问题
{
han(n-1,A,C,B);
move(A,n,C);
han(n-1,B,A,C);
}
}
int main()
{
han(3,'A','B','C'); //将三层的汉诺塔从A盘移到C盘
}
三、快速排序、归并排序
1.快速排序(手写快排用于面试)
快速排序是由冒泡排序改进而得的。在冒泡排序中,只对相邻的两个记录进行比较,因此每次交换两个相邻记录时只能消除一个逆序。而快速排序可以通过两个不相邻记录的一次交换,来消除多个逆序。
- 任取一个元素为中心
-
所有比它小的元素一律前放,比它大的元素一律后方,形成左右两个子表
-
对各子表重新选择中心元素并依此规则继续形成左右两个子表(递归的思想)
- 直到每个子表元素只剩一个
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]的值交换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换;
5)重复第3、4步,直到i == j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i ==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
排序演示
假设一开始序列{xi}是:5,3,7,6,4,1,0,2,9,10,8。
此时,ref=5,i=1,j=11,从后往前找,第一个比5小的数是x8=2,因此序列为:2,3,7,6,4,1,0,5,9,10,8。
此时i=1,j=8,从前往后找,第一个比5大的数是x3=7,因此序列为:2,3,5,6,4,1,0,7,9,10,8。
此时,i=3,j=8,从第8位往前找,第一个比5小的数是x7=0,因此:2,3,0,6,4,1,5,7,9,10,8。
此时,i=3,j=7,从第3位往后找,第一个比5大的数是x4=6,因此:2,3,0,5,4,1,6,7,9,10,8。
此时,i=4,j=7,从第7位往前找,第一个比5小的数是x6=1,因此:2,3,0,1,4,5,6,7,9,10,8。
此时,i=4,j=6,从第4位往后找,直到第6位才有比5大的数,这时,i=j=6,ref成为一条分界线,它之前的数都比它小,之后的数都比它大,对于前后两部分数,可以采用同样的方法来排序。
代码实现:
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int n;
void Qsort(int l, int r)
{
if (l >= r) return;
int i = l - 1;
int j = r + 1;
int key = a[l];
while (i< j)
{
do i++;while(a[i] < key) //从左向右找比key大的值
do j--;while(a[j] > key) //从右向左找比key小的值
if(i < j) swap(a[i], a[j]);
}
Qsort(l, j);
Qsort(j+1, r);
}
int main()
{
cin >> n;
for(int i=0 ; i<n; i++) cin >> a[i];
Qsort(0, n-1);
for(int i=0; i<n; i++) cout << a[i] << " ";
return 0;
}
【算法分析】
(1)时间复杂度:平均时间复杂度是:O(nlogn)
空间复杂度:
(2)最优的情况下空间复杂度为:O(logn)
最差的情况下空间复杂度为:O( n )
【算法特点】
(1)记录非顺次的移动导致排序方法是不稳定的
(2)排序过程需要定位表的下界和上界,所以适合用于顺序结构,很难用于链式结构
(3)当n较大时,在平均情况下快速排序是所有内部排序方法中速度最快的一种,所以其适合初始记录无序、n较大时的情况
快排视频讲解链接 http://www.proedu.com.cn/web/shareVideo/index.action?id=1013492&ajax=1
2.归并排序
归并排序(Merging Sort)就是将两个或两个以上的有序表合并成一个有序表的过程(2-路归并)。
【归并排序算法的思想】
假设初始序列含有 n 个记录,则可以看成是 n 个有序的子序列,每个子序列的长度为 1,然后两两归并,得到 [ n/2 ]个长度为 2 或 1 的有序子序列;再两两归并,… ,如此重复,直至得到一个长度为 n 的有序序列为止。
如以下GIF所示:
【分治策略:分而治之】
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
【核心操作】
将待排序序列中前后相邻的两个有序序列归并为一个有序序列
相邻的两个有序序列的归并 http://www.proedu.com.cn/web/shareVideo/index.action?id=1013497&ajax=1
类似于顺序有序表的合并 http://www.proedu.com.cn/web/shareVideo/index.action?id=1013418&ajax=1
【代码实现】
#include <iostream>
using namespace std;
const int N = 1000010;
int a[N], tmp[N];
int n;
void mergesort(int l, int r)
{
if(l > r) return;
int mid = (l+r) / 2; //把当前序列一分为二
//对子序列递归归并排序
mergesort(l, mid);
mergesort(mid+1, r);
// 再将两个序列归并
//tmp[]存 l~mid 和 mid+1~r 合并后的有序数组
//l ~ mid mid+1 ~ r
//i j
int k=0, i=l, j=mid+1;
while(i<=mid && j<=r)
{
if(a[i] < a[j])
tmp[k++] = a[i++];
else
tmp[K++] = a[j++];
}
//特殊情况是像 6789和 234 这样排完一组了,另一组还没动
while(i <= mid) tmp[k++] = a[i++];
while(j <= r) tmp[k++] = a[j++];
//最后将存入排好序的tmp数组赋给a数组
for(int i=l,j=0; i<=r; i++,j++) a[i] = tmp[j];
}
int main()
{
cin >> n;
for(int i=0 ; i<n; i++) cin >> a[i];
Qsort(0, n-1);
for(int i=0; i<n; i++) cout << a[i] << " ";
return 0;
}
四、DFS、BFS
1. DFS:深度优先搜索
走路嘛,遇到岔路选一条走到底,然后回头到上一个岔路走另一条,…,一直重复到遍历完毕
深度优先搜索遍历连通图:http://www.proedu.com.cn/web/shareVideo/index.action?id=1013466&ajax=1
深度优先搜索遍历非连通图:
http://www.proedu.com.cn/web/shareVideo/index.action?id=1013467&ajax=1
采用邻接矩阵表示图的深度优先搜索遍历:
http://www.proedu.com.cn/web/shareVideo/index.action?id=1013468&ajax=1
采用邻接表表示图的深度优先搜索遍历:
http://www.proedu.com.cn/web/shareVideo/index.action?id=1013469&ajax=1
2. BFS:广度优先搜索
这种遍历类似于树的按层次遍历的过程,像阅兵一样,一排一排看 ,一直到看完为止
广度优先搜索遍历连通图:
http://www.proedu.com.cn/web/shareVideo/index.action?id=1013470&ajax=1
总结
这就是本周学习的内容,其中DFS和BFS重点也是难点,还是要再多看几遍视频来加深理解的,相对来说递归和快排还好一点,就是还得练题练代码
最后还得提一下认真仔细这件事,周赛的那两道题其实也就是自己不够认真出现的小毛病导致的 ,再仔细看看也就发现了,写的时候别着急,键盘敲稳了要,可别再出现“错别字漏笔划”的问题了