ACM寒假训练--第三周学习总结


前言

《数据结构 C语言版 | 第2版》
• 二叉树(P111)
• 递归(P61)
• 快速排序(P243)、归并排序(P254)
• DFS(深度优先搜索 P161)、BFS(广度优先搜索 P163)


一、二叉树

树结构是一类重要的非线性数据结构,直观来看,是以分支关系定义的层次结构。
树的结构定义是一个递归的定义,即在树的定义中又用到了树的定义,它道出了树的固有特性。

1.树的定义

(Tree)是 n(n>=0)个结点的有限集,它或为空树(n = 0);或为非空树,对于非空树 T :

  1. 有且只有一个称之为根的结点;
  2. 除根结点以外的其余结点可分为 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:

  1. 有且只有一个称之为根的结点;
  2. 除根结点以外的其余结点分为两个互不相交的子集 T1 和 T2 ,分别成为 T 的左子树和右子树,且 T1 和 T2 本身又都是二叉树。

二叉树与树的区别:

  1. 二叉树每个结点至多只有两颗子树(即二叉树中不存在度大于 2 的结点);
  2. 二叉树的子树有左右之分,其次序不能任意颠倒。

二叉树的 5 种基本形态:

  1. 空二叉树
  2. 仅有根结点的二叉树
  3. 右子树为空的二叉树
  4. 左子树为空的二叉树
  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)定义是递归的

  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);  //递归步骤
}
  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 塔问题
在这里插入图片描述

  1. 准备工作
    为了便于描述算法,将搬动操作定义为 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. 递归

(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所示:
在这里插入图片描述

【分治策略:分而治之】
vhj哦哦iu偶i哦iu偶偶哦偶
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为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重点也是难点,还是要再多看几遍视频来加深理解的,相对来说递归和快排还好一点,就是还得练题练代码
最后还得提一下认真仔细这件事,周赛的那两道题其实也就是自己不够认真出现的小毛病导致的 ,再仔细看看也就发现了,写的时候别着急,键盘敲稳了要,可别再出现“错别字漏笔划”的问题了

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值