王道一休的数据结构

本文是王道一休关于数据结构的总结,重点关注数组、链表、树的代码题,讲解了如何在考试中高效解题。内容包括常用代码优化技巧,如最大最小值宏定义、比较函数、输入输出函数的简化,以及快速排序、折半查找等算法。同时,文章提供了多种解题策略,如暴力解法、优化解法,并分析了它们的时间和空间复杂度。此外,文章还强调了在处理数据结构问题时,如何利用已知条件优化算法的重要性。
摘要由CSDN通过智能技术生成

数组、链表、树代码题总结——一休

如果文档中有什么问题可以找王道班主任或者直接找我。
本来我想讲的东西很多,希望能够把所有可能用到的东西都展示给大家(比如引用,如果理解不了,考试中怎么通过其他方法避免使用引用),但是时间紧张,之后会考虑制作对应课程包,不同的同学想听的东西不同,大家有什么建议以及这几次课程的评价都可
以填问卷或者直接告诉我或班主任,感谢大家的支持,希望你们能学会这种分析做题的方法,那代码题拿分甚至拿满分都是没问题。数据结构其他部分每一小块我也在做总结,但是这个很难做,你们可以去看一下习题视频里面的红黑树和并查集部分,希望可以帮助大家复习。
【腾讯文档】算法大题课程反馈及建议
https://docs.qq.com/form/page/DRXBpY0xJa0RtZUtT
0 思维导图

1 考试时如何写代码
考试中的注释也是必不可少的,因为重要的是让老师理解我们的思路,所以在关键位置处写注释有助于老师阅读。①在变量的定义处告知该变量是做什么的;②在使用伪代码处和函数调用处告知这部分实现哪些功能;③代码逻辑复杂的地方告知是做什么的。
很多简单的代码、库函数写起来浪费时间,考试时简写或者写个系统调用即可,只要让老师知道你想干嘛即可,但重要的代码一定不能这样,否则是0分。比如说本题就是考察快速排序,你直接调用Qsort不把快排过程写出来,当然拿不到分。那如何判断?只要这个东西的代码有可能超过你的所有代码1/4(其实就是他很重要),就一定要写。
给大家总结了一些可以偷懒节省时间的地方,考试时简写即可,使用的时候写清楚他的功能即可。
1.1 最大最小值Max_int、Min_int
从名字可以看出来,Max_int是最大的整数值,Min_int是最小的整数值(同理,float、double、unsigned也可以这样定义),他常用于比较选择最大最小值时,比如初值设为Max_int,那遇到任意值都会比他小或者相等,这样只要遇到比他小的就更新为更小的值。
       int D_min=Max_int;            //D_min初始设为最大的整数
   for (int i=0; i<n; i++)       //遍历D循环选出最小的D
      if (D[i]<D_min)
          D_min=D[i];            //D[i]比D_min小,D_min更新

1.2 比大小函数max(a, b)、min(a, b)
从名字可以看出来,这两个函数是为了得到a和b中的最小值和最大值
函数定义如下(不用掌握):
int max(int a, b){
   if(a>b)
      return a;
   else return b;
   }
int min(int a, int b){
   if(a<b)
      return a;
   else return b;
   }
使用这两个函数能让我们的代码变得简洁、清晰:
       int D_min=Max_int;            //D_min初始设为最大的整数
   for (int i=0; i<n; i++)       //遍历D循环选出最小的D
      D_min=min(D_min, D[i]);    //D_min=D_min和D[i]中的较小值

1.3 输入输出函数cin、cout
C语言中的输入、输出函数分别是scanf和printf函数,因为和变量的类型有关,写起来比较麻烦,在考试中可以用C++,那我们可以使用C++的cin、cout函数来输入、输出,而不用管变量的类型,会简洁很多。
cin>>A[0];                   //读入A[0],cin是左箭头
cin>>b>>c;                   //读入b、c
cout<<A[0];                  //输出a
cout<<b<<c<<endl;            //输出b、c,并输出回车
scanf(“%d”, A);            //读入A[0]
scanf(“%d”, &b);             //读入b

使用cin、cout函数不用管是不是要加&,也不用管他是什么类型的变量,可以节省考试时间,也减少出错可能:
       int D_min=Max_int;            //D_min初始设为最大的整数
   for (int i=0; i<n; i++)       //遍历D循环选出最小的D
      D_min=min(D_min, D[i]);    //D_min=D_min和D[i]中的较小值
   for (int i=0; i<n; i++)
      cout<<A[i]<<” ”;          //循环输出数组A以及空格
      
当然还有一种更简单的写法,写中文。比如直接写输出数组A、输入数组A:
       int D_min=Max_int;            //D_min初始设为最大的整数
   for (int i=0; i<n; i++)       //遍历D循环选出最小的D
      D_min=min(D_min, D[i]);    //D_min=D_min和D[i]中的较小值
   for (int i=0; i<n; i++)
      输出数组A

1.4 A[i++]和A[++i]
这两个写法是不一样的,A[i++]中i在前表示先使用i,再i++;A[++i]中先i++,再使用A[i](此时i是i++后的结果)。这两种写法相当于两步缩写成了一步,如果记不住还是拆开写成两步稳妥。
比如对于cout<<A[i++],相当于先用再加:
cout<<A[i];                  //输出A[i]
i++;                         //i自增
比如对于cout<<A[++i],相当于先加再用:
i++;                         //i自增
cout<<A[i];                  //输出A[i]

1.5 交换函数swap(a, b)
函数定义如下(不用掌握):
int swap(int *a, b){
   int temp=a;
   a=b;
   b=temp;
   }
让我们看看这两种不同的写法,①不使用swap函数:
       for(int i=n-1; i>1; i–)
      for (int j=0; j<i; j++)       //冒泡排序
         if (A[i]<A[j]){           //交换A[i]和A[j]
             temp=A[i];
             A[i]=A[j];
             A[j]=temp;
          }
②使用swap函数:
       for(int i=n-1; i>1; i–)
      for (int j=0; j<i; j++)       //冒泡排序
if (A[i]<A[j]){           //交换A[i]和A[j]
         swap(A[i], A[j]);         //交换A[i]和A[j]
2 复杂度问题
如无特殊说明,数据结构大题中问的时间和空间复杂度都是最坏情况下的复杂度,只有一个例外:快速排序(考虑平均复杂度,或者给快速排序增加一种优化)。
在计算机学科中,log2n常常写作logn,他们都是以2为底的对数运算。
2.1 时间复杂度
       时间复杂度是对时间增长速度的一个估计。时间复杂度为O(f(n))表示该算法执行的指令数的上界是O(f(n))级别的,可以简单的用极限理解,若所有指令总执行次数T,时间复杂度为O(f(n))的算法表示=K(K为常数)。
例如T1=n2+n,T2=2n2-n,通过极限的思想得到这两个算法的f(n)=n即时间复杂度都是O(n),常数K1=1,K2=2,K1≠K2,但复杂度是一样的,所以常数系数不影响复杂度;本例中时间复杂度只有最高次决定,所以较低次项不影响复杂度。总之我们就是需要查看算法的指令执行次数T的最高次项(而不考虑他的系数)。
O(log2n)和O(log2n)其实是相等的,因为log2n=log23
log3n,而log23是常数,所以O(log2n)=O(log2n),考虑复杂度时,所有的log都尽量写成log2n或者logn的形式。
对于考试时写的代码,时间复杂度只需要考虑循环和递归次数。
循环次数
建议尽量写for循环,所有循环都可以写成for循环
对于多层for循环,如果每一层的变量都是自增1,则直接把每层执行次数相乘
   for (i=0;i<n;i++)
      for (j=0;j<n;j++)
          for (k=0;k<n;k++)
             if (A[i][j]>A[i][k]+A[k][j])
                 A[i][j]>A[i][k]+A[k][j];
他的每一层都是变量自增,且变量都是从0~n-1共n次,所以总次数是n3,时间复杂度是O(n3)。

对于多层for循环,如果每一层的变量都是自增1,但第二三层变量取值范围和第一层变量有关
   for (i=0;i<n;i++)
      for (j=0;j<i;j++)
          for (k=0;k<j;k++)
             if (A[i][j]>A[i][k]+A[k][j])
                 A[i][j]>A[i][k]+A[k][j];
时间复杂度和第①种一样,但不用考虑具体次数,时间复杂度是O(n3)。

对于两层循环,变量不是自增1(这种情况主要针对小题)
   for (i=1;i<n;i*=2)                  //22考研真题
      for (j=0;j<i;j++)
          sum++;
需要讨论第一层变量i取不同值时,j的取值有x种,然后对x求和。比如本题,i=1, 2, 4, 8…2k(2k<n≤2k+1)时,第二层j有i个取值,所以总次数是对i求和,即T=1+2+4+2k=2k+1-1,n<T<2n,时间复杂度就是O(n)。

递归次数
递归总次数和时间复杂度有关,而最大递归层数和空间复杂度有关。
对于树中序遍历的递归写法,每个节点都会调用一次所以总共调用次数是O(n)级别的,时间复杂度是O(n),而递归使用的层数最多是树高h,空间复杂度是O(h)。
    void inorder(BTNode *p){
          if (p==NULL) return;
          inorder(p->lchild);
          visit§;                           //对p节点的访问等
          inorder(p->rchild);
      }

2.2 空间复杂度
只算额外空间开销,不考虑题目中已经给出的数据所占的空间,比如题目中给出了一个数组,这个数组是不计入空间复杂度的。
定义的数组、链表
单个定义的变量都不需要考虑,但定义的数组中元素个数以及链表中节点个数都要计入空间复杂度中。
递归层数
是在递归栈中最多出现多少个过程函数,层数和总次数比较见时间复杂度的递归次数。

2.3 注意题目关于复杂度的要求
共有五种情况:
时间上尽可能高效:表示只要求时间复杂度,对空间复杂度不做要求
时间和空间两方面都尽可能高效:时间和空间复杂度都要求尽量小,时间复杂度小优先。
尽可能高效:要求同(2)。
空间复杂度为O(1)且时间上尽可能高效的算法:要求算法的空间复杂度是O(1),时间复杂度尽可能小。
没有描述:只要能做出来就行,不要求复杂度是多少,常见于树中
3 顺序表
推荐流程:想出暴力解法->分析暴力解法复杂度->在暴力解法基础上思考优化
暴力解法是最容易想到的做法,思维难度小,复杂度高(主要是时间复杂度),通常情况下都不会是最优解,比如这题满分是15分,下面是比较:
所花时间 得分
暴力解法 5分钟 11分
最优解 15分钟,不一定能做出来 15分
       如果我们孤注一掷的求最优解可能最后连暴力解的分都拿不到,做过真题的同学会发现有一些年份的最优解是比较难的,在考试中很难做出来,所以不管怎么样至少都要拿到暴力解法的分数。
①对于基础不够好的同学目标就是做出暴力,如果正好自己能够做出来更优解就写,以节省时间为主。②对于基础牢固的同学,目标应该是先想出暴力解放有个保底分,然后在暴力的基础上做出改进优化后的算法,即保暴力分,冲满分。③对于经常刷机试题、算法扎实的同学目标就应该是最优解,但如果实在想不到,还是应该写暴力解。
       如果我们不满足于暴力解法,就应该想,暴力解法浪费了什么,我们在什么地方可以对他优化,优化一般要使时间或者空间复杂度减小(主要是时间复杂度),比如说题目给的是有序数组,但是我们没有用到有序性,这是有条件没用;或者题目不需要排序,只要求中位数,但是我们对他排序了,超额完成任务,本来可以不做这么多的,这就是浪费。最好的情况就是我们完美的利用了题目的条件,而且又不多做一丁点事,这样的算法一般都更优秀。
3.1 暴力解法
枚举法
       枚举法是最容易想到的方法,把所有可能的情况都考虑到,然后从中选出符合题目要求的情况,通常使用for循环遍历。
      
对无序数组排序
       对于一个无序的数组可以先通过排序把他变成有序再处理,排序使用快速排序。考试中直接默写快速排序(注意这是升序!),然后注意调用快速排序时的参数即可。
快速排序是一种分治思想,每一轮快排分为两个步骤:①选择枢值key。②枢值移动到他的最终位置,左右划分为两个区间,左边区间的所有元素都小于枢值,右边区间的所有元素都大于枢值。然后对左右区间分别进行快排,不断重复直到当前处理区间元素个数小于等于1(即L>=R)。
快速排序的平均时间复杂度是O(nlogn),平均空间复杂度是O(logn),是考试中最快的不稳定排序算法,一般要用到排序时都使用快速排序。快速排序的最坏时间复杂度是O(n2),最坏空间复杂度都是O(n),但我们只需要加一个小优化就能避免最坏情况:即随机选择一个元素作为枢值。优化后最坏时间复杂度O(nlogn),最坏空间复杂度O(logn)。
(注:快速排序有很多写法,交换式和挖坑式,不同写法中间过程不一样但只要能实现排序即可,本代码适用于算法大题,方便记忆)
代码如下:
    void Qsort(int A[], L, R){      //a数组保存数据,L和R是边界
   if (L>=R) return;             //当前区间元素个数<=1则退出
   int pivot, i=L, j=R;            //i和j是左右两个数组下标移动
   把A数组中随机一个元素和A[L]交换  //快排优化,使得基准值的选取随机
   pivot=A[L];                    //pivot作为基准值参与比较
   while (i<j){
      while (i<j && A[j]>key)
          j–;
      while (i<j && A[i]<=key)
          i++;
      if (i<j)
          swap(A[i], A[j]);      //交换A[i]和A[j]
   }
   Swap(A[L], A[i]); //将基准值放入他的最终位置
/此时A[Li-1]<=A[i]<=A[i+1R]/
   Qsort(A, L, i-1);         //递归处理左区间
   Qsort(A, i+1, R);          //递归处理右区间
      }

3.2 思考优化的方向
       主要是三个方向:①有没有条件没有使用到,如有序性,那如何利用这个条件?②有没有超额完成题目中没有要求的任务,比如题目要求求最小值,但我们却将他排序了,这是没有必要的,那如何不做这个任务?③除了暴力的思考方向,还有没有别的方向?

3.3 进阶需要掌握的算法
利用有序性——折半查找
       在线性表中如果需要查找权值为x的元素,时间复杂度是O(n),也就是说不管什么线性表,查找元素的复杂度下界是O(n)。如果要实现查找的时间复杂度是O(logn),因为待查找元素可能出现在任意位置,那需要每次查找都能够排除一半情况,这就是折半查找的逻辑。折半查找前提:①数列必须是有序的,升序或者降序;②只能是顺序表(数组),不能是链表。
       考试中当我们看到出现了一个有序数组(如果是链表一定不能使用)的时候,首先想想是否需要查找,能不能使用折半查找,这是经常考察的点,2011、2018、2020年都可以使用折半查找。
       折半查找,也叫二分查找,假设我们在升序数组A[LR]中查找x,L和R是上下界(即Left和Right),mid=(L+R)/2,每次把x与A[mid]比较:如果x>A[mid],说明x一定不会出现在A[Lmid],只可能出现在A[mid+1R],更新L=mid+1;而如果x<=A[mid],说明x一定不会出现在A[mid+1R],只可能出现在A[L~mid],更新R=mid。重复比较——更新过程,直到L==R,此时A[L]就是我们要找的元素,如果A[L]不等于x说明x不在数组A中。
(注意:①书写折半查找,最怕当只有两个元素的出现死循环,书写完后一定要带入只有两、三个元素的时候试一下是否有问题;②如果是题目中返回查找成功时的下标可以直接使用这段代码,否则需要调整最后一个if语句。)
代码如下:
int Binary_Search(int A[], L, R, x){ //A[]和x不一定是int型
   int mid;
   while (L<R){                     //如果L>R则范围错误
      mid=(L+R)/2;                 //mid取中间数,向下取整
      if (x<=A[mid]) R=mid;
          else L=mid+1;             //更新查找范围
   }
   if (A[L]==x) return L;           //查找成功,返回数组下标L
      else return -1;                     //查找失败
}
       时间复杂度为O(logn),空间复杂度为O(1)。

设置多指针后移
多指针后移常用于有序线性表(顺序表和链表都可以),如果考试中给出有序的线性表,优先考虑这种方式,这种方式可以用于合并多个有序线性表、查找第k个元素等等,归并排序就是用到了这种思想。
       例题:
定义距离d = |x – y|,给定2个长度为n的升序数组A、B,x是数组A中的某个元素,y是数组B中的某个元素。请设计一个尽可能高效的算法,计算并输出所有可能的的最小距离D。例如 数组A = {–16, –8, 5, 8, 13},B ={-2, 0, 2, 6, 10},则最小距离为1,相应的x=5, y=6, d= |5 – 6|=1。
这是2个升序数组,查找一种最小的情况,可以考虑使用指针后移,设置两个指针(实际上是两个int变量)i和j保存此时正在处理的数组A、B元素的下标,每次只比较a[i]和b[j],数组A和B都是升序,每次i或j后移都会导致x或y变大,最终要求的是最小的d,所以我们的每次选择i还是j后移的时候原则就是尽量不让d变大,即要让a[i]和b[j]中较小的那个后移。初始时i=j=0, x=-16, y=-2, d=14,a[i]小,应该i后移即i++,这样会缩小x和y的差距,而如果j后移会继续拉大x和y的差距。①如果i++,则x=-8, y=-2,d=6;②如果j++,则x=-16, y=-0,d=16。我们的目的是求出最小的d,所以②这种情况是没意义的,选①情况,每次比较完x和y后把较小的那个值指针后移。最终每一个数组都只会遍历一次,不会回头,所以时间复杂度是O(n)。

3.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值