前言
开始前我们要知道顺序表和链表是什么,顺序表和链表都是线性表的一种。
线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是⼀种在实际中⼴泛使
⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储。
案例:蔬菜分为绿叶类、⽠类、菌菇类。线性表指的是具有部分相同特性的⼀类数据结构的集合
简单来说,线性表是具有相同特性的一类数据结构的统称,这里的相同特性是指在逻辑结构上线性,但在物理结构上不一定线性
(数据结构是指计算机存储,组织数据的方式)
题目讲解
好了,在对顺序表和链表有了一定了解后,让我们开始做题吧
移除元素
要点解读
- 题目要求不能使用额外的数组,只能在原有的数组上进行修改,最后返回修改后数组的元素个数
思路
- 我们可以用指定位置删除的思路,但这样的话我们就先需要进行定位,再进行删除的操作,这样的话就需要两次循环比较麻烦。所以我们采用双指针的办法
int removeElement(int* nums, int numsSize, int val){
int src=0;//用来遍历数组
int dest=0;//指向删除指定元素后的数组
while(src<numsSize)
{
if(nums[src]==val)
{
src++;
}
else
{
nums[dest]=nums[src];
dest++;
src++;
}
}
return dest;
}
解析
(我们创建了两个整型变量,并不是指针,但为什么还叫双指针法呢?
那是因为我们是在原有数组上进行操作只需要知道元素对应下表即可,本质上是一样的,而且题目最后要求返回的是元素个数,这样操作也会更加简便)
- src用来遍历数组,当src对应的值等于删除元素后,我们直接让src++,跳过该元素来达到删除目的。当src对应的值不等于删除元素时,我们将数组中src对应的值赋给数组中dest位置,再将src,dest都++以此进行正常的循环。最后将修改后数组个数即dest返回
合并两个有序数组
要点解读
- 题目要求合并两个非递减顺序的数组,即递增数组,合并后也为递增数组。
- 第一个数组长度为m+n,第二个数组长度为n。
- m为第一个数组个数,n为第二个数组元素个数。
思路
- 要将两个升序数组合并为一个升序数组,我们应该从小到大,还是从大到小排呢?答案是从大到小,因为如果从小到大排的话,就会有元素被覆盖,导致元素丢失;而且第一个数组在一开始后n个数据都是无效数据,所以应该比大,而且从后面开始往前排
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int l1=m-1;
int l2=n-1;
int l3=m+n-1;//要比大,比小会导致数据别覆盖,从空位置开始操作
while(l1>=0&&l2>=0)
{
if(nums1[l1]<nums2[l2])
{
nums1[l3--]=nums2[l2--];
}
else
{
nums1[l3--]=nums1[l1--];
}
}
while(l2>=0)
{
nums1[l3--]=nums2[l2--];
}
}
解析
- 我们是按比大,从后往前的思路进行排序,所以先创建三个变量
- l1指向第一个数组有效元素的最后一位
- l2指向第二个数组元素的最后一位
- l3指向第一个数组最后一个位置
- 通过循环,让第一,第二个数组开始比较大小,将每轮较大的元素排在第一个数组l3的位置
- 那么循环结束的条件是什么呢
- 我们的目的是将第二个数组的元素都放在第一个数组里面,所以l2>=0是我们的一个循环条件
- 假如第二个数组元素都比第一个数组元素小,那么仅仅凭l2>=0是不够的。所以l1>=0也是循环条件
- 当退出循环时,我们并不能确保一定是因为l2>=0这个条件而退出循环,所以我们要再次对l2>=0这个条件进行判断,从而确保完成整个排序要求
移除链表元素
要点解读
- 给了一个链表,删除指定的元素,最后返回新的头节点
思路
- 可以在原链表上操作,定义一前一后指针,将要删除元素的前一个节点和后一个节点连接起来
- 我们也可以创建新的链表,将符合要求的元素放入。
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val){
ListNode*newhead=NULL;
ListNode*ptail=NULL;//创建新的链表并置为空
ListNode*pcur=head;//遍历链表
while(pcur)
{
if(pcur->val!=val)
{
if(newhead==NULL)//对于新头节点的操作
{
newhead=pcur;
ptail=pcur;
}
else
{
ptail->next=pcur;
ptail=ptail->next;
}
}
pcur=pcur->next;//让pcur走向下一个节点,达到遍历链表的目的
}
if(newhead)//如果为空则无需操作,否则将链表最后一个节点的next置为空
{
ptail->next=NULL;
}
return newhead;//返回新的节点
}
解析
- 先定义两个指向头尾的链表newhead,ptail并置为空,用pcur来遍历原链表
- 用pcur作为循环条件(当pcur不为空时进入循环,否则结束循环)
- 当pcur里的val不等于要删除的val时,将pcur赋给ptail->next,从而将新的链表连接起来
- 要注意先对新链表的头节点的操作,即将第一个pcur里val不等于指定val的pcur赋给newhead
- 最后记得一定要把尾节点ptail的next置为空,否则
- 因为没有对尾的next进行置空,仍连接着下一个节点,所以即使ptail指向的是5节点,但仍然会输出6
反转链表
要点解读
- 反转链表并返回反转后的链表
思路
- 定义一个新的空链表,遍历原链表,进行头插操作,最后返回新的链表,这是一种思路,但比较麻烦。
- 所以,我们采用反转链表的思路,通过改变指针的指向关系,来反转链表。
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
if(head==NULL)
{
return NULL;
}
ListNode*n1,*n2,*n3;
n1=NULL,n2=head,n3=head->next;
while(n2)//n3不行,这样的话n1,n2还没完成
{
n2->next=n1;
n1=n2;
n2=n3;
// n3=n3->next; //会提前出循环,导致最后一次时已经是空无法->next
if(n3)
{
n3=n3->next;
}
}
return n1;
}
解析
-
首先要对原链表进行一下判断,如果为空,则返回NULL。
-
创建三个指针n1,n2,n3,其中n1为空,n2为头节点,n3为头节点的下一个节点,如图
-
按上图所示,将n2的next指向n1,这样就完成了指针指向的反转,之后将n1,n2,n3往后移,从而完成一次循环
- 在将n1,n2,n3往后移的时候,需要对n3进行判断是否为空,因为n3会在循环结束前就走到空,这时候就不能再进行n3=n3->next的操作了(已经空了,还能进行解引用操作吗?显然是不能的,OJ也会报错),当n3为空时不再进入if语句,但不影响程序。
-
那么什么时候完成反转,退出循环呢?还是从图里看,最后一次完成反转后n2走出了循环,所以n2可以作为循环的条件
-
最后返回n1完成本题
合并两个有序链表
要点解读
- 合并两个有序链表,并返回合并后的链表
思路
- 可以按照上题移除链表元素的思路,创建新的链表,再按升序的顺序将原先两个链表的节点有序连接起来
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
if(list1==NULL)//要先对原有的连个链表进行判断
{
return list2;//注意要交叉返回,即判断list1返回list2,因为当list1为空时,
} //list2不一定为空。
if(list2==NULL)
{
return list1;
}
ListNode*pcur1=list1;//用来遍历链表
ListNode*pcur2=list2;
ListNode*head,*ptail;//创建新链表的头和尾并置为空(NULL)
head=ptail=NULL;
while(pcur1&&pcur2)
{
if(pcur1->val < pcur2->val)
{
if(head==NULL)//判断新链表是否为空链表
{
head=pcur1;
ptail=pcur1;
}
else{//新链表不为空
ptail->next=pcur1;//让链表的尾往后走
ptail=ptail->next;
}
pcur1=pcur1->next;//指向下一位,也让循环正常进行
}
else{ //pcur1->val > =pcur2->val
if(head==NULL)
{
head=pcur2;
ptail=pcur2;
}
else{
ptail->next=pcur2;
ptail=ptail->next;
}
pcur2=pcur2->next;
}
}
if(pcur1)//当循环结束时,还会有一个链表没有连接到新链表上,所以判断是那条链表
{ //并接上新的链表上
ptail->next=pcur1;
}
if(pcur2)
{
ptail->next=pcur2;
}
return head;
}
拓展与思考
- 整段代码看下来比较简单,但是在头节点是否为空这里的代码高度重复,使整段代码看起来非常繁琐。
- 那有没有解决的办法说呢?
- 答案是创建带头的链表
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
if(list1==NULL&&list2==NULL)//与上面的写法有点不同,可以思考一下为什么
{
return NULL;
}
ListNode*phead,*ptail;//创建头尾节点,头节点(哨兵位)只用来存放下一个节点的地址,不存储有效数据
phead=ptail=(ListNode*)malloc(sizeof(ListNode));
while(list1&&list2)
{
if(list1->val<=list2->val)
{
ptail->next=list1;//用ptail来连接新的节点,并往后走到尾
ptail=ptail->next;
list1=list1->next;
}
else
{
ptail->next=list2;
ptail=ptail->next;
list2=list2->next;
}
}
if(list1)
{
ptail->next=list1;
}
else
{
ptail->next=list2;
}
ListNode*rethead=phead->next;//phead->next才是我们所需链表的头
free(phead);//对于堆区开辟的节点,用完要及时释放,防止内存泄漏
return rethead;
}
链表的中间结点
要点解读
- 返回链表中间节点,奇数个节点时返回中间节点,偶数个节点时返回第二个中间节点
思路
- 采用快慢指针的办法
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
if(head==NULL)
{
return NULL;
}
ListNode*slow,*fast;//创建快慢指针并指向头节点
slow=fast=head;
while(fast&&fast->next)//不能交换
{
slow=slow->next;//进入循环一次,慢指针走一步
fast=fast->next->next;//快指针走两布
}
return slow;
}
解析
- 唯一需要注意的地方时循环的判断条件
- 当节点为奇数个时,当slow走到中间节点时,fast走到最后一个,所以可以用fast->next作为循环条件
- 当节点为偶数个时,当slow走到中间节点的下一个时,fast走到空,所以可以用fast作为循环条件
- 需要注意的是循环条件(fast&&fast->next)的顺序是不能被调换的 (如果只考虑奇数个节点时就可以),否则
- 这是因为当节点个数为偶数时,最后一次循环后fast就已经为空了,再次进入循环条件判断时就会形成在空指针里找next(这也是上面报错的原因),这显然是不合法的
环形链表的约瑟夫问题
约瑟夫问题介绍
- 题目要求只剩下一人,意味着m只能等于1或2,与下面介绍事例有点出入
要点解读
- n为总人数,m为出局号数
思路
- 从题目提供的接口可以看出需要我们创建一个环形链表
- 要点
- 创造环形链表
- 出局方式
typedef struct ListNode ListNode;
ListNode*ListBuyNode(int x)//申请节点
{
ListNode*node=(ListNode*)malloc(sizeof(ListNode));
if(node==NULL)
{
perror(malloc);
exit;
}
node->val=x;
node->next=NULL;
return node;
}
ListNode*ListNodeCreat(int n)
{
ListNode*phead=ListBuyNode(1);//先创建一个节点,方便等会连接
ListNode*ptail=phead;
int i;
for(i=2;i<=n;i++)//通过循环将节点连接起来
{
ListNode*node=ListBuyNode(i);
ptail->next=node;//*
ptail=ptail->next;
}
ptail->next=phead;//连接成环
return ptail;//返回尾节点
}
int ysf(int n, int m ) {
ListNode*prev=ListNodeCreat(n);//用prev接受尾节点
ListNode*pcur=prev->next;//尾的next即头节点,即pcur
int count =1;
while(pcur->next!=pcur)//游戏结束时只能剩下一人
{
if(count==m)//出局
{
prev->next=pcur->next;
free(pcur);
pcur=prev->next;
count=1;
}
else {//不会有人出局,使两个都节点往下一个节点就好,记得要将count++
prev=pcur;
pcur=pcur->next;
count++;
}
}
return pcur->val;
}
解析
-
环形链表的创建
- 根据题目要求,我们需要创建n个节点来代表人数,并为其从1开始安排到n的编号,再将这些带编号的节点按照升序的排序连接起来。
- 知道目的后就开始创建环形链表
- 为了创建环形链表,我们封装了一个ListNodeCreat(n)函数,并把要创建的人数个数传过去
- 在ListNodeCreat(n)里我们先创建一个头节点方便一会操作
- 创造节点又需要申请节点,所以我们又封装了一个ListBuyNode(i)函数,i为要插入节点数据域的编号
- 需要注意的就是在ListNodeCreat函数结束时将首尾连接起来 (ptail->next=phead),此时我们就将环形链表创建好了,我们此时返回的是尾节点(ptail),所以在接受时要区分得清
-
出局方式
- 游戏是通过报数的方式进行的,当报的数为m时出局,所以我们创建一个计数变量int count =1,每轮都加1模拟报数。
- 当count=2时,pcur指向的节点就要出局,前一个节点**(prev)的next就要指向pcur的下一个节点(pcur->next),再释放pcur节点,再让新的pcur走向下一个节点。记得要将count置为1**
-
游戏结束时只剩一个节点,所以用pcur->next!=pcur作为循环的控制条件。
分割链表
要点解读
- 将原有链表重新排序,并返回新链表
思路
- 创建两个大小链表,将小于值X的节点放入小链表,大于等于值X的节点放入大链表
typedef struct ListNode ListNode;
struct ListNode* partition(struct ListNode* head, int x){
if(head==NULL)
{
return NULL;
}
ListNode*lessHead,*lessTail;
ListNode*greaterHead,*greaterTail;
lessHead=lessTail=(ListNode*)malloc(sizeof(ListNode));//创建大小链表的头和尾
greaterHead=greaterTail=(ListNode*)malloc(sizeof(ListNode));
ListNode*pcur=head;
while(pcur)
{
if(pcur->val < x)//小于X,放入小链表
{
lessTail->next=pcur;
lessTail=lessTail->next;
}
else //大于等于X,放入大链表
{
greaterTail->next=pcur;
greaterTail=greaterTail->next;
}
pcur=pcur->next;
}
if(greaterTail->next)
{
greaterTail->next=NULL;//将大链表的next置空,确保新链表有尾
}
lessTail->next=greaterHead->next;//大小链表连接起来
free(greaterHead);//将此时已无用的头节点释放,防止内存泄漏(也可以说是好习惯)
ListNode*rethead=lessHead->next;
free(lessHead);//同上
return rethead;
}
解析
- 有了前面 合并两个有序链表 的前车之鉴,为了避免代码的重复性,我们创建头节点(哨兵位),只存放下一个节点的地址,不存放有效数据
- 将大小链表连接好之后,将小链表的尾与大连表连接起来,注意,要记得将大链表的尾的next置为空。
- 此时大链表的最后一个节点为5,但在原链表里5的下一个节点是连接这2的,这样就会使链表没有尾,从而程序无法停止。
总结
- 在这六道经典的链表题目中,大都和链表节点的重新排序有关,其中移除链表元素,合并两个有序链表,分割链表,都创建新的链表,这样既简单容易理解又迅速,大家可以好好理解这个方式,希望会在日后做链表或者相关题型时有所帮助
结语
这是本人第一篇博客,希望对大家有点帮助;第一次写博客还有很多不足,希望大佬们多多指出不足,共同进步。谢谢!