关于数据结构代码题,可以说是让很多同学感到头疼了,书上的代码太繁琐、网上的总结不全面让大家对代码题感到云里雾里,那么这篇文章可能会给大家带来一点启发,因为我自己也是深受代码题的折磨,所以一直想写一篇有关它的总结,希望能够做到全面、简洁,让大家用最快的速度记住代码书写思路、解答出题目、拿到更高的分数。
下面我将先对写代码所需要的基础知识进行总结,然后从两方面展开对代码题的分解,一方面是数据结构定义,另一方面是算法书写,最后通过历年真题验证我们的代码书写思路。
文章目录
- 前言 基础知识
- 一、数据结构定义
- 二、算法书写
- 后语 历年真题(未完... ...)
前言 基础知识
代码作为一种和计算机交流的语言,要遵守程序语言世界的规范,接下来我们全部采用C语言书写代码。
(一)把代码题解决需要几步
完成一个代码题共有两步:
- 写出相应的数据结构:如果说写代码题是建房子,那么相应的数据结构就是建房子用的砖瓦,例如顺序表、链表、树、图等;
- 写出算法代码:利用定义的数据结构,可以根据题目中给出的任务写出相应的C语言代码。
(二)需要知道的C语言表达
1.定义变量
规范表达:
DataType 变量名 = 赋值内容
DataType可以是常见数据类型int、string等,也可以是结构体比如LNode,还可以是指针LNode*、LinkList,变量名可以是单个数据,也可以是数组。
············································································································································
关于数组的定义,可以这样表达:
DataType 变量名[Maxsize]
············································································································································
如果要定义常量,使用如下语句:
#define 常量名 值
············································································································································
关于LNode*、LinkList这两者的区别如下:
lnode* p的含义:p是一个指针,它指向lnode类型的变量,它是一个lnode型变量的地址;
linklist l的含义:l是一个指针,它指向表头lnode类型的变量,它是表头lnode类型变量的地址。
可以说LNode * == LinkList
············································································································································
关于指针的表达,我总结出这样一句话来概括:
指针指向对象:LNode * p,出现其中一个时就是它本身,出现其中两个时缺谁就代表谁,出现三个时表示指针
p是指针,*代表指向,LNode是对象,所以说是指针指向对象;
出现其中一个就是它本身,比如单写一个p,就是指针p,单写一个LNode,就是结点;
出现两个时,比如写*p,就是指缺的那个LNode,也即代表指向的结点;
再如写LNode *,就是指缺的那个p,也即代表指向结点的指针;
三个都出现时,比如LNode * p,就代表p指针本身。
············································································································································
再说一下指针的赋值,简单来说就是令两指针的指向相同:
lnode *s=L 表示:s指针指向L指针指向的内容,也即s与L同指向,而不是s指向L,s不是指针的指针。
其他数据类型的赋值是 “值 = 值”,而指针的赋值是 “地址 = 地址”,也即令两指针的指向相同。
············································································································································
我们有时能看到这样的两种表达:p.a和p->a,它们直接有什么区别呢?先来看一个例子:
typedef struct // 定义一个结构体类型:DATA
{
char key[10]; // 结构体成员:key
char name[20]; // 结构体成员:name
int age; // 结构体成员:age
}DATA;
DATA data; // 声明一个结构体变量
DATA *pdata; // 声明一个指向结构体的指针
// 访问数据操作如下:
data.age = 24; // 结构体变量通过点运算符( . )访问
pdata->age = 24; // 指向结构体的指针通过箭头运算符( -> )访问
总结:
->:用在结构体指针的后面,p->a代表结构体指针p指向的结构体所具的a属性;
. :用在结构体后面(".“这个运算符可以理解为中文里"的”),p.a代表结构体p的a属性。
2.定义函数
函数无疑是整段代码的主体,最重要的解题逻辑都包含在函数中,如何书写一个函数呢?请看下文。
规范表达:
返回值类型 函数名(参数类型 参数名, 参数类型 参数名... ...){
变量定义;
代码主体;
}
根据函数的表达,在书写一个函数前要确定这样几件事情:
- 返回值类型:如void、int、string、LNode、LinkList等;
- 定义一个能表述函数作用的函数名;
- 确定需要输入进函数的参数;
- 书写函数内部逻辑,包含定义变量和代码主体。
3.动态分配语句
在需要用到动态分配的时候,比如要对L.p(L结构体定义的指示动态分配数组的指针)进行动态分配,就用到下面的代码:
L.p = (DataType *)malloc(Initsize*sizeof(DataType))
很多同学会觉得这个代码很难记,其实它的逻辑很简单:L.p是指示动态分配数组的指针,指向DataType类型的数据,所以它的类型为DataType *,malloc函数的含义是开拓一片新的空间,空间大小为initsize个单位,每个单位大小为sizeof(DataType)。
一、数据结构定义
在书写函数之前,首先要定义数据结构,也就是定义所需要用到的结构体,在数据结构定义中,顺序结构基于数组进行定义,链式结构基于指针进行定义。
结构体的定义方式如下:
struct 结构体名{
结构体属性
}
不过这种定义方式存在一些问题,就是在使用的时候必须要用这样的定义:
struct 结构体名 结构体对象
需要多写一个struct,很繁琐,所以可以使用这样一种重命名方式:
typedef struct{
结构体属性
}结构体别名
这样每次在使用的时候就可以这样用:
结构体别名 结构体对象
有以下基本数据结构的结构体定义:
有一个小妙招,就是在定义一个数据结构之前,先根据其概念将其表示成图,然后根据图定义结构体,这样可以更形象化。
(一)线性表(List)
1.顺序表(SqList/SeqList)
概念:用顺序存储方式存储(物理结构)的线性表(逻辑结构)。
图示:
结构体定义:
1)静态分配方式(SqList)
顺序结构基于数组,根据图示,我们想想,在结构体里都要定义什么呢?首先要定义一个存放数据的数组,然后还得再定义一个当前数组长度,除此之外,还要设置一个常量MaxSize用以表示数组的最大长度,就OK啦,那我们来根据结构体的定义方式来试着定义下:
#define MaxSize 50 //定义常量MaxSize为50
typedef struct{
DataType data[Maxsize]; //定义存放数据的数组
int length; //定义当前长度
}SqList
2)动态分配方式(SeqList)
一维数组可以是静态分配的,也可以是动态分配的。对数组进行静态分配时,因为数组的大小和空间事先已经固定,所以一旦空间占满,再加入新数据就会产生溢出,进而导致程序崩溃。
而在动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,将原表中的元素全部拷贝到新空间,从而达到扩充数组存储空间的目的,而不需要为线性表一次性地划分所有空间。
动态分配方式定义顺序表:
#define InitSize 50 //定义常量InitSize为50
typedef struct{
DataType *data; //定义指向存储空间的指针
int MaxSize,length; //定义最大长度和当前长度
}SeqList
//实例化
SeqList L;
//初始化存储空间
L.data = (DataType*)malloc(InitSize*sizeof(DataType))
2.链表(LinkList/DLinkList/CLinkList/SLinkList)
用链式存储方式存储(物理结构)的线性表(逻辑结构),用指针一个个链接结点。
1)单链表(LinkList)
顾名思义,单链表就是两个结点之间只有单个指针链接的链表。
图示:
结构体定义:
根据图示,我们来想一想都要在结构体中定义什么呢?首先要定义一个数据域,然后再定义一个指针域(指向结点的指针),如下:
typedef struct{
DataType data; //定义数据域
struct LNode *next; //指向下一个结点的指针
}LNode,*LinkList
对于链表,课本上有各种操作的代码,比如头插法、尾插法、删除结点、加入结点等等,但其实只需要掌握好指针指向结点这一关系,就不用记住那么多代码。
2)双链表(DLinkList)
顾名思义,双链表就是两个结点之间有双向指针链接的链表。
图示:
结构体定义:
根据图示,我们来想一想都要在结构体中定义什么呢?首先要定义一个数据域,然后再定义两个指针域(指向前后结点的指针),如下:
typedef struct{
DataType data; //定义数据域
struct LNode *prior,*next; //指向下一个结点的指针
}DNode,*DLinkList
3)循环链表(CLinkList)
顾名思义,循环链表就是首尾有指针链接的链表。
图示:
4)静态链表(SLinkList)
静态链表是用数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与前面所讲的链表中的指针不同的是,这里的指针是结点在数组中的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。
图示:
结构体定义:
根据图示,我们来想一想都要在结构体中定义什么呢?首先要定义一个数据域,然后再定义一个指针域(用数组下标表示),如下:
#define MaxSize 50 //静态链表的最大长度
typedef struct{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
(二)栈和队列(Stack、Queue)
1.栈(Stack)
1)顺序栈(SqStack)
采用顺序结构存储的栈称为顺序栈,顺序结构用数组存储。
图示:
结构体定义:
根据图示,我们来想一想都要在结构体中定义什么呢?首先要定义一个存储数据的数组,并设置其MaxSize,然后再定义一个栈顶指针(用数组下标表示),如下:
#define MaxSize 50 //顺序栈最大长度
typedef struct{ //静态链表结构类型的定义
DataType data[MaxSize]; //存储数据元素的数组
int top; //栈顶元素下标
}SqQueue;
2)链栈(LinkStack)
采用链式结构存储的栈称为顺序栈,链式结构借助指针存储,和链表非常相似。
图示:
结构体定义:
根据图示,我们来想一想都要在结构体中定义什么呢?首先要定义一个数据域,然后再定义一个指针域(指向下一个结点),如下:
typedef struct{ //链栈定义
DataType data; //存储数据元素
struct LNode *next; //下一个元素的指针
}LSNode,*LinkStack;
2.队列(Queue)
和栈完全一致,只不过把存储栈顶指针变成了队首指针、队尾指针。
1)顺序队(SqQueue)
采用顺序结构存储的队列称为顺序队列,顺序结构用数组存储。
图示:
结构体定义:
根据图示,我们来想一想都要在结构体中定义什么呢?首先要定义一个存储数据的数组,并设置其MaxSize,然后再定义队尾指针和队首指针(用数组下标表示),如下:
#define MaxSize 50 //顺序队列最大长度
typedef struct{ //顺序队列类型的定义
DataType data[MaxSize]; //存储数据元素的数组
int head,rear; //栈顶元素下标
}SqQueue;
2)链队(LinkQueue)
采用链式结构存储的队列称为顺序队列,链式结构借助指针存储,和链表非常相似。
图示:
结构体定义:
根据图示,我们来想一想都要在结构体中定义什么呢?首先要定义一个数据域,然后再定义一个指针域(指向下一个结点),如下:
typedef struct{ //链队定义
DataType data; //存储数据元素
struct LNode *next; //下一个元素的指针
}LQNode,*LinkQueue;
(三)串
1)顺序串(SString)
定义:采用顺序存储方式的串。
结构体定义:
#define MAXLEN 255 //预定义最大串长为255
typedef struct{
char ch[MAXLEN]; //每个分量存储一个字符
int length; //串的实际长度
}SString;
2)堆分配串(HString)
定义:采用堆分配存储方式的串。
结构体定义:
typedef struct{
char *ch; //按串长分配存储区,ch指向串的基地址
int length; //串的长度
}HString;
3)链串(String)
定义:采用链式存储方式的串。
结构体定义:
//每个结点一个字符
typedef struct StringNode{
char ch;
struct StringNode * next;
}StringNode,*String;
//每个结点n个字符
typedef struct StringNode{
char ch[n];
struct StringNode * next;
}StringNode,*String;
(四)树和二叉树(Tree、BiTree)
1.树(Tree)
1)孩子表示法(CTree)
定义:孩子表示法是将每个结点的孩子结点都用单链表链接起来形成一个线性结构。
图示:
结构体定义:
根据图示,可知需要定义三种结构体:结点、孩子结点、树。
结点中需要定义数据域、指针域(指向第一个孩子结点)
孩子结点中需要定义数据域(存放孩子结点在数组中的位置)
树中需要定义结点数组、结点数和根的位置
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct{
int child; //孩子结点在数组中的位置
struct CTNode *next; //下一个孩子
}CTNode;
typedef struct{
ElemType data; //结点中储存的数据元素
struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r; //结点数和根的位置
}CTree;
2)双亲表示法(PTree)
定义:这种存储结构采用一组连续空间来存储每个结点 , 同时在每个结点中增设一个伪指针 , 指示其双亲结点在数组中的位置。
图示:
结构体定义:
根据图示,可知需要定义两种结构体:树和结点
结点中需要定义数据域和指针域(数组下标表示)
树种需要定义结点数组、结点总数
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct{ //树的结点定义
ElemType data; //数据元素
int parent; //双亲位置域
}PTNode;
typedef struct{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
3)孩子兄弟表示法(CSTree)
定义:孩子兄弟表示法又称二叉树表示法 , 即以二叉链表作为树的存储结构,左孩子右兄弟。
图示:
结构体定义:
根据图示,可知只需要定义一个链树:
链树包含以下两个内容:结点内容、左孩子右孩子指针
typedef struct{
DataType data;
struct CSNode *lchild,*rchild;
}CSNode,*CSTree
2.二叉树(BiTree)
1)顺序二叉树(SqBiTree)
与动态分配方式定义顺序表的方式相同,在结构体中先定义一个指向存储空间的指针,再定义空间的大小。
图示:
结构体定义:
typedef struct{
DataType *data;
int length;
}SqBiTree
2)链式二叉树(LinkBiTree)
和孩子兄弟表示法类似,由于顺序存储的空间利用率较低,因此二叉树一般都采用链式存储结构,用链表结点来存储二叉树中的每个结点。在二叉树中,结点结构通常包括若干数据域和若干指针域,二叉链表至少包含3个域:数据域data、左指针域lchi1d和右指针域rchild,如图5.5所示。
图示:
结构体定义:
根据图示,可知只需要定义一个链树:
链树包含以下两个内容:结点内容、左孩子右孩子指针
typedef struct BiTNode (
ElemType data; // 数据域
struct BiTNode *lchild, *rchild; // 左 、 右孩子指针
}LinkBiTNode,*LinkBiTree;
3)线索二叉树(ThreadBiTree)
定义:二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索 。 而前驱或后继的信息只有在遍历时才能得到 , 因此线索化的实质就是遍历一次二叉树 。
操作:
InThread(ThreadTree &p,ThreadTree &pre)
PostThread(ThreadTree &p,ThreadTree &pre)
PreThread(ThreadTree &p,ThreadTree &pre)
图示:
结构体定义:
除了要定义两个指针外,还要定义两个线索标志。
typedef struct ThreadNode (
ElemType data; // 数据元素
struct ThreadNode *lchild, *rchild; //左 、 右孩子指针
int Itag, rtag; // 左 、 右线索标志
} ThreadNode , *ThreadTree
(五)图(Graph)
在大题种最常用、常考的是邻接矩阵法和邻接表法。
1)邻接矩阵法(MGraph)
图的邻接矩阵(Adjacency Matrix) 存储方式是用二维数组来表示图。
图示(无向图):
结构体定义:
定义一个二维数组存储结点之间的边相连情况,定义一个一维数组存储结点编号。
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum,arcnum; //图当前的顶点数与边数
}MGraph;
2)邻接表法(ALGraph)
定义:图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
图示:
结构体定义:
类似孩子表示法,需要定义三个结构体,结点、边结点、图。
#define MaxSize 100 //顶点数目的最大值
typedef struct{
int node; //指向的结点位置
int weight; //权值
struct ArcNode *next; //下一条边的指针
}ArcNode
typedef struct{
DataType data; //数据域
ArcNode *first; //指向第一条边的指针
}VNode
typedef struct{
VNode vnode[MaxSize];
int vetexnum,arcnum; //图的顶点数与边数
}ALGraph
3)邻接多重表法(AMLGraph)
定义:邻接多重表是无向图的另一种链式存储结构。
图示:
4)十字链表法(OLGraph)
定义:十字链表是有向图的一种链式存储结构。
图示:
二、算法书写
虽然书上有很多算法代码,但是其实在考试时用到的大多是以下几类,我大致进行了总结,希望以最简洁、最通俗的方式展示给大家,后续如果有遗漏也会不断补充~
对于数据结构代码题,我总结的经验就是:一,抓住真题,把每一道真题的各种解法做透;二,接受次优,时空复杂度最优的解往往要付出大量的思考时间和书写时间,在考试上来说是得不偿失的,在想不出最优或者最优解很难写的时候,选择次优解才是更好的选择,在平时训练时当然要弄懂最优解,但是在考试的时候就不要那么精益求精啦,毕竟次优解也就扣个两三分;三,融会贯通,接下来讲的方法都只是解题的砖瓦,而解题就像搭房子,要把砖瓦堆叠起来,而不是仅仅用一块砖一块瓦。
大家要记住一句话:顺序表基于数组,链表基于指针。
说明:由于不同题目要求返回值不同,以下函数代码不对返回值进行规定,只书写基本代码逻辑。另外由于暴力解没有统一格式,下面的讲解中不会讲暴力解,具体真题的暴力解会在真题部分进行讲解。
(一)顺序表类
相较于链表,顺序表考频较低,不过基于顺序表的很多常用算法(后面会讲),如折半查找、快速排序、计数排序都会用到顺序表。如果一个题目基于数组且没有用到查找、排序等算法,那它就可以归为顺序表类题目。
顺序表类题目通常会用到以下几种解题思路:遍历、逆置法、双指针法。我们逐一来讲解。
1.遍历
遍历是最常用的办法,只有遍历,才能对顺序表数组中的元素进行判断,看其是否符合要求。
书写遍历代码,首先想个函数名,比如Traverse,那么要输入什么呢?顺序表保存在数组中,所以输入一个数组,再输入数组长度,在函数体书写中,加入判断逻辑,看数组元素是否符合要求。
Traverse(DataType A[],int n){
for(int i=0;i<n;i++){
if(A[i]符合条件){
执行代码
}
}
}
2.逆置法
有的题目要求对顺序表进行逆置,或者局部对换,如将(a1,a2,a3,……an-1,an)转换为(an-1,an,……a1,a2,a3)这种时候就要用到两次逆置法。逆置法的输入是一个数组,还有逆置的起始下标和结束下标。
Reverse(DataType A[],int l,int r){
DataType temp;
for(int i = 0;i < (r - l + 1)/2;i++){
temp = A[l + i];
A[r + i] = A[l - i];
A[l - i] = temp;
}
}
例如2010年真题。
3.指针法
常用双指针法来同时锁定两个数据的位置并对其进行操作。
例如
(二)链表类
1.遍历
与顺序表的遍历类似,对于链表的遍历需要输入链表头指针,依次对链表中结点的数据域进行判断。
Find(LinkList* head){
for(LNode* s = head->next;s != NULL;s = s->next){
if(判断内容){
执行代码
}
}
}
2.逆置法
链表问题经常用到逆置,这涉及到头插和尾插的转换。
例如
3.指针法
常用双指针法来同时锁定两个数据的位置并对其进行操作。
例如2009年真题。
(三)树类
树类问题中的前序、中序、后序遍历均采用递归算法,分为链树和顺序树,层序遍历采用迭代算法。
1.链树
链树的遍历比较简单,直接采用指针进行遍历即可,无非就是一个把“visit”放在哪个位置的问题。
二叉链树的定义:
typedef struct BiTNode (
ElemType data; // 数据域
struct BiTNode *lchild, *rchild; // 左 、 右孩子指针
}BiTNode,*BiTree;
(1)判空
bool Empty(BiTree* Node){
if(Node->lchild == NULL && Node->rchild == NULL) return true;
return false;
}
(2)前序遍历
void PreOrder(BiTree* Node){
if(isEmpty(Node)) return;
visit(Node);
PreOrder(Node->lchild);
PreOrder(Node->rchild);
}
(3)中序遍历
void InOrder(BiTree* Node){
if(isEmpty(Node)) return;
InOrder(Node->lchild);
visit(Node);
InOrder(Node->rchild);
}
(4)后序遍历
void PostOrder(BiTree* Node){
if(isEmpty(Node)) return;
PostOrder(Node->lchild);
PostOrder(Node->rchild);
visit(Node);
}
2.顺序树
顺序树的遍历稍微麻烦一点,涉及到寻找父节点、左右孩子的问题。
二叉顺序树的定义:
typedef struct{
DataType *data;
bool isEmpty; //左右孩子是否为空
}BiTree
(1)判空
bool Empty(BiTree Node[],int index){
return Node[index].isEmpty;
}
(2)获取左孩子
int getLchild(BiTree Node[],int index){
lchild = index * 2;(下标从1开始)/(index+1) * 2 - 1;(下标从0开始)
return lchild;
}
(3)获取右孩子
int getRchild(BiTree Node[],int index){
rchild = index * 2 + 1;(下标从1开始)/(index+1) * 2 + 1 - 1;(下标从0开始)
return rchild;
}
(4)获取父结点
int getParent(BiTree Node[],int index){
parent = index/2;(下标从1开始)/(index+1)/2 - 1;(下标从0开始)
return parent;
}
(2)前序遍历
void PreOrder(BiTree Node[],int index){
if(isEmpty(Node[],index)) return;
visit(Node[index]);
PreOrder(Node[],getLchild(Node[],index));
PreOrder(Node[],getRchild(Node[],index));
}
(3)中序遍历
void InOrder(BiTree* Node){
if(isEmpty(Node[],index)) return;
InOrder(Node[],getLchild(Node[],index));
visit(Node[index]);
InOrder(Node[],getRchild(Node[],index));
}
(4)后序遍历
void PostOrder(BiTree* Node){
if(isEmpty(Node[],index)) return;
PostOrder(Node[],getLchild(Node[],index));
PostOrder(Node[],getRchild(Node[],index));
visit(Node[index]);
}
3.层序遍历
顺序树的遍历更加麻烦一点,需要借助队列进行操作。这里对二叉链树进行层序遍历。
void LeverOrder(BiTree* Node){
Queue que; //定义队列
BiTree* cur; //指向当前结点
EnQueue(que, Node); //根节点入队
while(que != NULL){ //在队列不为空的时候
DeQueue(que, cur); //出队
visit(cur); //访问
if(cur->lchild != NULL) EnQueue(que, cur->lchild); //左孩子入队
if(cur->rchild != NULL) EnQueue(que, cur->rchild); //右孩子入队
}
}
(四)图类
1.深度优先遍历
对采用邻接表法存储的图进行深度优先遍历。
//深度优先
int visit[MAX]={O};
void DFS(ALGraph *G, int v){
visit[v]=1;
printf("%d",v);
p=G->vnode[v].first;
while(p!=NULL){
if(visit[p->node]==0)
DFS(G,p->node);
p=p->next;
}
}
2.广度优先遍历
对采用邻接表法存储的图进行广度优先遍历。
//广度优先
int visit[MAX]={O};
void BFS(ALGraph *G, int v){
visit[v]=1;
printf("%d",v);//输出被访问顶点的编号
SqQueue qu;
enQueue(qu,v);
int w;
while (!QueueEmpty(qu)){
deQueue(qu,w);
p=G->vnode[w].first; //指向w的第一个邻接点
while(p!=NULL){ //查找w的所有邻接点
if(visit[p->node]==0){
printf("%d",p->node);
visit[p->node]=1;
enQueue(qu,p->node);
}
p=p->next; //找下一个邻接点
}
}
}
(五)查找类
1.折半查找(二分查找)
二分查找基于有序顺序表,输入顺序数组和目标值,不断与中间值作比较,直至找到目标值。
int Search(int[] nums, int target) {
int left = 0, right = nums.Length - 1;
while (left <= right) {
int mid = (right - left) / 2 + left;
int num = nums[mid];
if (num == target) {
return mid;
} else if (num > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
(六)排序类
关于排序算法的详细内容,可以看我之前写的一篇文章,不过常考代码的主要是这三种:
1.快速排序
快排分为两部分:划分、快排主体。
void QuickSort(ElemType A[],int low,int high){
if(low<high){ //递归跳出的条件
//Partition()是划分操作,将表A划分为满足上述条件的两个子表
int p=Partition(A,low,high); //划分
QuickSort(A,low,p-1); //依次对两个子表进行递归排序
QuickSort(A,p+1,high);
}
}
int Partition(ElemType A[],int low,int high){ //一趟划分
ElemType pivot=A[low]; //将表中第一个元素设置为枢轴,对表进行划分
while(low<high){ //循环跳出条件
while(low<high&&A[high]>=pivot) --high;
A[low]=A[high]; //将比枢轴元素小的移动至左端
while(low<high&&A[low]<=pivot) ++low;
A[high]=A[low]; //将比枢轴元素大的移动至右端
}
A[low]=pivot; //枢轴元素存放至最终位置
}
2.计数排序
void CountSort(ElemType A[],ElemType B[],int n,int k){
int i,c[k];
for(i=0;i<k;i++){
c[i]=0; //初始化计数数组
}
for(i=0;i<n;i++){ //遍历输入数组,统计每个元素出现的次数
C[A[i]]++; //c[A[i]]保存的是等于A[i]的元素个数
}
for(i=1;i<k;i++){
c[i]=C[i]+C[i-1]; //C[x]保存的是小于或等于x的元素个数
}
for(i=n-1;i>=0;i--){ //从后往前遍历输入数组
B[C[A[i]-1]]=A[i]; //将元素A[i]放在输出数组 B[]的正确位置上
C[A[i]]=C[A[i]]-1;
}
}
3.归并
在考试中主要会用到归并操作,用于将多个各自有序的数组合并为一个有序数组。
void Merge(int A[], int m, int B[], int n, int C[], int l){
int i=j=k=0;
while(i<m && j<n){
if(A[i] <= B[j]) C[k++]=A[i++];
else C[k++]=B[j++];
}
while(i<m) C[k++]=A[i++];
while(j<n) C[k++]=B[j++];
}
后语 历年真题(未完… …)
说明:每道题目几乎都有暴力解,暴力解可视为最差解,得分最低,最好选择最优解或者次优解。
总结:
1.题目中有”要求时间尽量高效“,表示可以牺牲空间换时间;
2.如果代码逻辑中涉及计算位置差值,可以自己设计一个例子辅助计算。
(一)2009
首先可以判断这是一个链表类题目,链表的结构体已经给出定义,所以我们不需要再进行定义了。
题目要求查找链表中倒数第k个位置上的结点,由于不知道链表长度,从而也就不知道倒数第k个位置上的结点,所以我们首先能够想到的一种解题思路就是遍历两次链表,第一次确定链表长度n,第二次找第(n-k+1)个结点,并输入结点的数据域值。这种方法为次优解(不过其时空复杂度与最优解是相同的,所以也可以视为最优解)。
还有一种思路就是使用指针法,一指针在前,另一指针在后,下标差值为k-1,这样只通过一次遍历即可确认倒数第k个结点,并输出其data域的值。这种方法为最优解。
代码:
次优解:两次遍历:
int Ans(LinkList* list, int k){
int n = 1; //用于计结点数
LinkList* nlist = list;
while(nlist->link != NULL){
n++;
nlist = nlist->link;
}
if(n >= k){
int i = 0;
while(i < (n - k)) {
list = list->link;
}
printf(list->data);
return 1;
}
return 0;
}
最优解:指针法:
int Ans(node* list, int k){
node* p=q=list;
for (int i=0; i<k; i++){
q=q->link;
if (q==null)
return 0;
}
while (q!=null){
p=p->link;
q=q->link;
}
printf(p->data); //输出倒数第k个元素的值
return 1;
}
(二)2010
(三)2011
(四)2012
(五)2013
(六)2014
(七)2015
(八)2016
(九)2017
(十)2018
(十一)2019
(十二)2020
(十三)2021
(十四)2022
(十五)2023
(十六)2024
写在后面
这个专栏主要是我在学习408真题的过程中总结的一些笔记,因为我学的也很一般,如果有错误和不足之处,还望大家在评论区指出。希望能给大家的学习带来一点帮助,共同进步!!!
参考资料
[1]王道408教材(2025版)
[2]王道课程资料