Note-1

本文深入探讨了编程中的递归概念,将其与循环进行对比,并通过汉诺塔等例子来说明递归的逻辑。此外,文章还涵盖了数据结构的基础,如链表、栈、队列、映射,以及在解决实际问题如列车调度中的应用。通过抽象思维和伪代码,解释了如何理解和重写递归函数,以及如何利用数据结构高效解决问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

覆盖的知识点

语法

  • 递归

数据结构

  • 链表
  • 栈&&队列
  • 二叉树
    • BST
    • AVL
    • 线段树
    • 红黑
    • 二叉堆

算法

  • 排序
  • BFS/DFS
  • Dijkstra/Floyd
  • LCA
  • 并查集

基础语法

基础题

递归

  • 将递归看作深度不定,循环体可拆开的循环
      递归类似于循环,但循环一般(不全是)要求给定一个深度,也就是循环次数。而且,循环结构遵循:“判断”->“循环体”->“判断”->…的流程,除非中途跳出,循环体必须从头开始全部执行
      相对而言,递归基本上不需要给定深度(当然还是需要跳出递归的判断条件的),而且也可以在执行了一部分代码后,先进入下一层递归,等到递归全部处理完后才执行剩余的代码。
      其实,尾递归(也就是递归函数在自己的末尾)等价于循环,是可以简单地用循环重写的。

  • 从伪代码角度去理解
      另一方面,如果无法理解递归函数的执行逻辑,就尝试脱离具体的语句,将函数抽象为伪代码的形式。一般而言,递归函数的格式为:

whatever_type recursion( paraments ){
	
	//terminal condition ( return before enter the next recursion )
	
	//what need to do before
	recursion( new_paraments );
	// what need to do after recursions
	return sth.;// if you need
}

  这样抽象考虑的重点在于,不要“进入”递归函数,只要知道递归函数实现了它要做的事情就足够了,具体怎么实现的,在此时不需要考虑。这样,能够帮助我们厘清递归前后要做什么,以及参数要如何更新。
  以汉诺塔为例:(这里借用wiki的代码)

# 北京八维研修学院人工智能学院1906A班学员
# 2021年1月20日 星期三 下午17:28
# 听许朋飞老师讲解python基础之递归函数后做出
# 不足之处请各位不吝赐教
# 本代码侧重展示解题细节,没有刻意追求效率

xpillars = {'A', 'B', 'C'}  # 汉诺塔的三个柱子


def xhanoi(xtop, xbottom, xsrc='A', xdest='C'):
    '''
    解汉诺塔

    :param xtop: 移动的“整体”的最顶层,只能是1或xbottom
    :param xbottom: 移动的“整体”的最底层,正整数
    :param xsrc: 从哪个柱子移动(起始柱子)
    :param xdest: 移动到哪个柱子(目标柱子)
    :return: None
    '''
    if not (xtop == 1 and xbottom >= xtop or 1 < xtop == xbottom) \
            or not (xsrc in xpillars) \
            or not (xdest in xpillars):
        print('非法参数')
        return
    if xtop == xbottom:  # 仅移动一个盘子
        print(xtop, xsrc, '-->', xdest)
    else:  # 移动两个以上盘子
        xaux = (xpillars - {xsrc, xdest}).pop()  # 三个柱子里用作“中转”的柱子
        # 递归算法
        xhanoi(1, xbottom - 1, xsrc, xaux)  # 先把1到倒数第2个盘子移动到中转柱子
        xhanoi(xbottom, xbottom, xsrc, xdest)  # 移动最底下的盘子
        xhanoi(1, xbottom - 1, xaux, xdest)  # 再把1到倒数第2个盘子移动到目标柱子


if '__main__' == __name__:
    xhanoi(1, 3)  # 移动从1层到3层,从A到C。也就是求解3层汉诺塔。

该代码结构大致如下:

xpillars = {'A', 'B', 'C'}  # 汉诺塔的三个柱子


def xhanoi(xtop, xbottom, xsrc='A', xdest='C'):

	# terminal condition
    if xtop == xbottom: 
        print(xtop, xsrc, '-->', xdest)
	
    else: 
    	# what need todo before recursion
    	# here's to find the intermediate pillar, which is the new parament
        xaux = (xpillars - {xsrc, xdest}).pop() 
        
		# recursion body
        xhanoi(1, xbottom - 1, xsrc, xaux)  # 先把1到倒数第2个盘子移动到中转柱子
        xhanoi(xbottom, xbottom, xsrc, xdest)  # 移动最底下的盘子
        xhanoi(1, xbottom - 1, xaux, xdest)  # 再把1到倒数第2个盘子移动到目标柱子
        
		# nothing need todo after it

抽象了具体的语法后,其结构大致如下:

def xhanoi(xtop, xbottom, xsrc='A', xdest='C'):
	
	# 盘子是不是能直接移动了 	# 判断是不是不需要继续递归了

	# 找中转柱子					# 更新参数	# 进入下一层递归前要做的事
	
	# 将xtop-xbottom代表的		# 递归主体
	# “大的”汉诺塔看作一个整体,
	# 整个移动到中转柱子上(不需要考虑具体怎么移动)

	# 结束了					# 递归结束后要做的事
  • 题目(这两道题已经品鉴得够多了,题干可以直接在网上找到)
    斐波那契:

    已知斐波那契数列的第0项,第1项分别为1,1。用递归求第n项。

    八皇后:

    国际象棋的皇后可以向着8个方向移动。如何能够在8×8的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。
    .
    更一般地,如何求解n皇后问题。

链表

  类似于数组,其不同之处是将索引(也就是下标)用更一般化的地址代替了。一般而言,链表节点分为两部分,一部分储存节点的数据(value),另一部分储存下一个节点的地址(address)。
在这里插入图片描述
  从逻辑上讲,链表和数组都是一个按顺序储存数据的序列。但是从内存,或者说物理,上讲,数组的数据都是连续储存的(因此数组又可以叫“线性表”),它的索引实际上代表着内存上“偏移”的距离。而链表,因为储存的是具体的地址,因此每个节点不一定要连续储存。
  不过,在竞赛中,我们需要吸收主要是用(各种各样的)地址来代替索引的这种思想。也就是说,用一层层的指示关系来实现在 O ( 1 ) O(1) O(1)内找到数据的目的。

  经典的链表节点结构如下:

struct node{
	T val;	//T means the type of val
	address next;
}

  但竞赛中为了方便,我们往往用这种方式表示链表:

int nxt[],root;
T val[];

例如一个结构为a->b->c->d的链表,可以直接用数组储存地址。其中val:
0 1 2 3
d b a c
如此,nxt:
0 1 2 3
-1 3 1 0
root=2;
这样,第一个节点的地址是root,我们想访问这个节点,只需要访问val[root]
  val[root]->val[2]->a;
而a的下一个节点地址是nxt[root]
  val[nxt[root]]->val[nxt[2]]->val[1]->b
实际用的时候赋中间值就可以了。
这样看着很多此一举,但在建立二叉树和图的时候,这样可以节省不少代码量(以及运算时间)。

  cpp的stl中,vector头文件相当于链表这一容器

  进一步地,索引不一定要是数字。(不过这一点没什么意义)

推广

  先进后出。类似于只有一端能进出的通道。
  递归都可以用栈重写。

  cpp的stl中,stack头文件包含了栈这一容器

struct stack{
	int val[],_size;
	int top(){return val[_size-1];}
	void push(int x){val[_size++]=x;}
	void pop(){_size--;}
	int size(){return _size;}
}
class stack:
	val=[]
	size=0
	int top(self):
		return self.val[self.size-1]
	void push(self,int x):
		self.val[self.size]=x
		self.size+=1
	void pop(self):
		self.size-=1
  • 数学上的性质
    给定一个push——pop序列,呈现局部的pop序列性质。
  • 彩虹瓶
  • 列车调度

队列

  先进先出。也就是一端进一端出的线性表。

  cpp的stl中,queue头文件包含了队列这一容器

struct queue{
	int val[],head,rear;
	int top(){return val[head];}
	void push(int x){val[rear++]=x;}
	void pop(){head++;}
}
class queue:
	val=[]
	head=rear=0
	int top(self):
		return self.val[head]
	void push(self,int x):
		self.val[self.rear]=x
		self.rear+=1
	void pop(self):
		self.head+=1

优先队列(堆)

映射(也就是python里的字典)

  考虑前文提及的不是数字的索引,我们可以直接抛开索引上“顺序”的概念(或者说,放松索引集合上全序关系的限制),直接建立“键-值”之间的对应关系。stl中的map用红黑树储存,对于规模为n的数据-索引,可以在 O ( log ⁡ n ) O(\log n) O(logn)内找到索引对应的键值。

  cpp的stl中,map头文件包含了映射这一容器

  • 烂键盘
    [hint:用map储存某个字符是否只出现了一次。当然,字符数有限,开一个256长度的数组储存更方便]

题目思路

列车调度

题目可以按照这样的方式考虑:车先按照输入顺序驶入某条轨道,然后再按降序依次驶出。
样例8 4 2 5 3 9 1 6 7的整个调度过程大概是这样:
在这里插入图片描述
我们用模拟的想法,可以写出来:

int main(){
	vector<int> arr(100001,100001);
	int cnt=1,n;
	cin>>n;
	while(n--){
		int t,i;
		cin>>t;
		for(i=0;i<cnt;i++)
			if(t<arr[i]){
				arr[i]=t;
				break;
			}
		if(i==cnt)arr[cnt++]=t;
	}
	cout<<cnt;
}

但这个大数据会超时,特别是顺序到来的列车,上边的代码非常慢。由于发现轨道末端的列车序号是递增的,我们考虑特判需要新开轨道的情况:

int main(){
	vector<int> arr(100001,100001);
	int cnt=1,n;
	cin>>n;
	while(n--){
		int t;
		cin>>t;
		if(t<arr[cnt-1])
            for(int i=0;i<cnt;i++){
                if(t<arr[i]){
                    arr[i]=t;
                    break;
                }
            }
        else arr[cnt++]=t;
	}
	cout<<cnt;
}

但是这个对随机大数据的运算速度并没有显著提升,甚至测试点3只能碰运气过。
究其原因,我们判断列车应驶入哪条轨道的语句——也就是第二重循环——是线性的,时间复杂度是 O ( n ) O(n) O(n),二重循环的总时间复杂度就是 O ( n 2 ) O(n^2) O(n2)
考虑到数据是有序的,我们考虑二分查找,这样时间复杂度就是 O ( log ⁡ n ) O(\log n) O(logn),总复杂度就是 O ( n log ⁡ n ) O(n\log n) O(nlogn)(当然用set或者heap是可以的,不过setupbound不一定能想起来)
二分代码如下:(注意跳出二分查找的条件)

#include<iostream>
#include<vector>
using namespace std;


int main(){
	vector<int> arr(100001,100001);
	int cnt=1,n;
	cin>>n;
	while(n--){
		int t;
		cin>>t;
		int l=0,r=cnt-1,m;  // Dichotomy to find the minimum number greater than t.
		while(l<=r){
			m=(l+r)/2;
			if(t<arr[m]&&(m==0||t>arr[m-1<0?0:m-1])){
				arr[m]=t;
				break;
			}
			else if(t<arr[m])r=m-1;
			else l=m+1;
		}
		if(m==cnt-1)++cnt;
	}
	cout<<cnt-1;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值