顺序表:
①移除元素;
②合并两个有序数组。
单链表:
①移除链表元素;
②反转链表;
③合并两个有序链表;
④寻找链表的中间节点;
(待续......)
⑤环形链表的约瑟夫问题;
⑥分割链表。
(一)顺序表算法题
(1)移除元素
链接:https://leetcode.cn/problems/remove-element
在不创建新数组的情况下实现val元素的移除,我们采用双指针法。

让源指针循环遍历所有下标元素,当元素为val时,是我们需要移除的,我们就不管了,让其等着被其他值覆盖(或者不在访问范围内),只让遍历的src++;当元素不为val时,是"新数组"需要的元素,就将nums[dst]=nums[src],赋值完后再dst++,src++,继续向下。
等遍历完成时,以dst为分界线,以[0,dst)为下标的元素构成的数组就是我们需要的"新数组",而以[dst,numsSize)为下标的元素不在访问之列,返回值就是dst。
如图:

int removeElement(int* nums, int numsSize, int val) {
int src = 0;
int dst = 0;
for(src = 0; src<numsSize ; src++)
{
if(val != nums[src])
{
nums[dst] = nums[src];
dst++;
}
}
//遍历完成
return dst;
}
注意:此方法叫"双指针法",但src、dst都是数组的下标数字,不是真正的指针变量。
(2)合并两个有序数组
链接:https://leetcode.cn/problems/merge-sorted-array
思路①
------先将num2中数据放入num1数组的后面,再使用排序算法使其呈递增排列。
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int i = 0;
for(i=0;i<n;i++)
{
//塞到num1的后面
nums1[i+m] = nums2[i];
}
//冒泡排序
i = 0;
for(i=0;i<n+m-1;i++)
//趟数
{
int j = 0;
for(j = 0;j<m+n-1-i;j++)
//每一趟都能排好一个数据的位置,比较的次数自然就能-i
{
if(nums1[j]>nums1[j+1])
//递增
{
int tmp = nums1[j];
nums1[j] = nums1[j+1];
nums1[j+1] = tmp;
}
}
}
}
但冒泡排序嵌套的for循环效率不高,我们看看思路②。
思路②
------从后向前进行比较后,再从后向前将数据元素放入num1数组的后方。

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--] = nums1[l1--];
}
else
{
nums1[l3--] = nums2[l2--];
}
}
//处理l1<0但l2>=0的情况
//注意是while(l2>=0),而不是if(l2>=0)
while(l2>=0)
{
nums1[l3--] = nums2[l2--];
}
}
(二)单链表算法题
(1)移除链表元素
链接:https://leetcode.cn/problems/remove-linked-list-elements
思路①
------遍历链表,当遇到val节点时就移除。
分支语句为三类:头结点的移除、非头结点的移除以及不移除。
struct ListNode* removeElements(struct ListNode* head, int val)
{
//思路:遍历原链表,将值为val的节点移除
struct ListNode* pcur = head;
struct ListNode* prev = head;
while (pcur)
{
//头结点的移除
if (pcur == head && pcur->val == val)
{
struct ListNode* next = pcur->next;
free(pcur);
pcur = next;
head = pcur;
}
else if ( pcur->val == val)
{
prev->next = pcur->next;
free(pcur);
pcur = prev->next;
}
else
{
prev = pcur;
pcur = pcur->next;
}
}
return head;
}
把头结点和非头结点的移除分成两种情况,是因为在头结点的移除中,我们需要改变head的值,而无论是非头结点还是不移除的情况下,头结点head的值是不变的。
错误呈现:
struct ListNode* removeElements(struct ListNode* head, int val) {
//思路:遍历原链表,将值为val的节点移除
struct ListNode* pcur = head;
struct ListNode* prev = head;
while (pcur)
{
//头结点的移除
if (pcur == head && pcur->val == val)
{
struct ListNode* next = pcur->next;
free(pcur);
pcur = next;
head = pcur;
}
if ( pcur->val == val)
{
prev->next = pcur->next;
free(pcur);
pcur = prev->next;
}
else
{
prev = pcur;
pcur = pcur->next;
}
}
return head;
}
本来应该是if......else if......else三个并列的分支语句,只会选择三种情况中的一种进入,但换成if....../if......else,这就是两个并列的分支语句了,会先判断是否要if,在判断是否要进入if......else......二者中的一者。
当链表中所有元素都需要移除时,if....../if......else的写法就会报错。在第一个if中,链表的所有节点都移除了,pcur=NULL,再进入第二个分支if......else......中时,在if的条件判断(pcur->val == val)中,要对pcur进行访问,但此时pcur已经为空了。
而对空指针进行访问是错误操作。

思路② (⭐)
------创建新链表,尾插值为val的节点到新链表中。
代码实现:
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val)
{
ListNode* newHead = NULL;
ListNode* newTail = NULL;
//"新链表"的头结点、尾结点
ListNode* pcur = head;
while(pcur)
{
//需要尾插时
if(pcur->val != val)
{
//新链表为空时
if(newHead == NULL)
{
newHead = newTail = pcur;
pcur = pcur->next;
newTail->next =NULL;
}
else
//链表不为空
{
newTail->next = pcur;
newTail = pcur;
pcur = pcur->next;
newTail->next =NULL;
}
}
//不需要尾插时
else
{
pcur = pcur->next;
}
}
return newHead;
}
思路详解:
虽然方法叫"创建新链表法",但此处并没有真正动态申请内存,仅仅只是创建了两个指针newHead和newTail,我们只是把需要保留的节点串在newHead和newTail之间(可以多个指针指向同一节点),形成新链表(链表是新的,但节点不是新的。)

起初,我们创建指针newHead和newTail,并置空,在创建pcur作为遍历原链表的指针,在第一次遇到pcur->val != val时,newHead和newTail都要指向pcur,且newTail->next要置空,继续遍历,pcur=pcur->next。
当遇到pcur->val == val时,不需要放入newHead和newTail之间,那就继续遍历,让pcur=pcur->next。如果又遇见pcur->val != val,newHead作为头结点,不需要改变,newTail是尾指针,要再次赋值,使newTail = pcur,但仅仅只是改动newTail的指向还不够。newHead指向原先的某次pcur,而newTail指向最新的pcur,首尾指针还是各自为营,并没有串起来,在使newTail = pcur之前,我们应该让newTail->next = pcur 。(第一次插入后,newTail也是新链表的头结点指针,通过newTail->next,我们能让头结点的后继指针,指向新的要被新插入的节点。)再将newTail->next置空。
※注意:此处不能写newHead->next = pcur!
虽然newHead->next = pcur也能让头结点的后继指针,指向新的要被插入的节点,但从此以后,新链表只会有两个节点------头节点和最后一次被插入的节点。
newTail->next可以让每个前一次被插入节点的后继指针指向要被新插入的节点,但newHead->next每次都只能让头结点指向新插入的节点,如果中间还有被插入的节点,都会被忽略。
错误呈现:①②③
①newHead->next = pcur
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
ListNode* newHead = NULL;
ListNode* newTail = NULL;
//"新链表"的头结点、尾结点
ListNode* pcur = head;
while(pcur)
{
//需要尾插时
if(pcur->val != val)
{
//新链表为空时
if(newHead == NULL)
{
newHead = newTail = pcur;
pcur = pcur->next;
newTail->next =NULL;
}
else
//链表不为空
{
newHead->next = pcur;
newTail = pcur;
pcur = pcur->next;
newTail->next =NULL;
}
}
//不需要尾插时
else
{
pcur = pcur->next;
}
}
return newHead;
}
注意事项里已经分析了,此处只是贴出错误代码,不再赘述。
②能将newTail->next =NULL;写在循环结束后吗?
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
ListNode* newHead = NULL;
ListNode* newTail = NULL;
//"新链表"的头结点、尾结点
ListNode* pcur = head;
while(pcur)
{
//需要尾插时
if(pcur->val != val)
{
//新链表为空时
if(newHead == NULL)
{
newHead = newTail = pcur;
pcur = pcur->next;
}
else
//链表不为空
{
newTail->next = pcur;
newTail = pcur;
pcur = pcur->next;
}
}
//不需要尾插时
else
{
pcur = pcur->next;
}
}
//能将newTail->next =NULL;写在循环结束后吗?
newTail->next =NULL;
return newHead;
}
提交之后,会报错------

当原链表为空时,pcur为空,进不去循环,直接执行newTail->next = NULL,可此时newTail也为空,对空指针进行访问是错误操作,如果我们想在循环结束之后再让newTail->next置空,需要先对newTail判空。(不能对pcur判空,无论原链表是否为空链表,执行到newTail->next = NULL时,pcur都为空)
正确代码:
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
ListNode* newHead = NULL;
ListNode* newTail = NULL;
//"新链表"的头结点、尾结点
ListNode* pcur = head;
while(pcur)
{
//需要尾插时
if(pcur->val != val)
{
//新链表为空时
if(newHead == NULL)
{
newHead = newTail = pcur;
pcur = pcur->next;
}
else
//链表不为空
{
newTail->next = pcur;
newTail = pcur;
pcur = pcur->next;
}
}
//不需要尾插时
else
{
pcur = pcur->next;
}
}
if(newTail)
newTail->next =NULL;
return newHead;
}
③能将else里的如下两条语句的位置进行颠倒吗?
pcur = pcur->next;
newTail->next =NULL;
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
ListNode* newHead = NULL;
ListNode* newTail = NULL;
//"新链表"的头结点、尾结点
ListNode* pcur = head;
while(pcur)
{
//需要尾插时
if(pcur->val != val)
{
//新链表为空时
if(newHead == NULL)
{
newHead = newTail = pcur;
pcur = pcur->next;
newTail->next =NULL;
}
else
//链表不为空
{
newTail->next = pcur;
newTail = pcur;
newTail->next =NULL;
pcur = pcur->next; 不能调转两条语句的位置
}
}
//不需要尾插时
else
{
pcur = pcur->next;
}
}
return newHead;
}
提交后,输出结果有误------

我在VS2022里调试之后,可以发现在在第二次插入2时会进入else分支语句,newTail->next = pcur;和newTail = pcur;都没问题,但我们执行newTail->next =NULL;时,是将pcur节点的后继指针置空了,等我们再执行pcur = pcur->next;时,就是将NULL赋给pcur了,就会跳出循环。
原先的顺序中,我们是先将pcur->next赋给pcur,再执行newTail->next =NULL;的,所以不会影响到pcur的值的变动。
所以颠倒语句顺序就是错的,这么看来,似乎将newTail->next =NULL;写在循环结束后,反而更方便?不过也要注意好对newTail判空。
(2)反转链表
链接:https://leetcode.cn/problems/reverse-linked-list
思路①
------创建count数出链表的节点个数,反转 count/2 次。
思路比较清晰,但实现却很复杂,我们需要让第一个节点和最后一个节点进行数据交换,再从两头靠拢,逐步将两头的节点数据一一交换,总共要反转count/2次。
从头向中间靠拢的节点数据交换是很容易的,只需要借助next指针向下走就行,但从后往中间靠拢的节点数据交换就很麻烦。单链表是单向的,next指针只能指向后一个节点,如果想找到前一个节点就需要额外处理,可这又怎么处理------麻烦!
思路②(⭐)
------创建新链表,将原链表节点一个个头插进新链表中。
这个思路和移除链表元素的第二个思路很像,只不过移除链表元素我们用的是遍历+尾插,而此处,我们用遍历+头插,而且思路更简单,反转链表中,原链表中的所有元素都需要头插,并不需要再进行数据判断后决定是否要插入。
依旧先明白,此处也只是创建了两个新节点指针,而不是真正创建了新节点。
既然要遍历,那就需要一个遍历指针pcur,当newHead和newTail还为空时,我们要做的是将pcur赋值给newHead和newTail,再将newTail->next置空,pcur继续向下走,但此处就出了问题。
我们之所以能通过pcur找到原链表的下一个指针,是因为pcur对应节点的next指针指向了下一个节点,但是我们在"newTail->next置空"语句中,让pcur节点的next指针发生了变化,pcu->next指向的不再是下一个指针了。
由此提醒我们,需要提前将pcur->next保存起来(创建临时变量next),无论是何时的插入都需要保存pcur->next和pcur遍历向下,所以我们将保存语句和pcur遍历语句写在while循环内、if......else语句外。
插完第一个节点后,newHead、newTail不再为空。
再进行插入时,newHead变动成新的pcur,要想让新链表能成功被串起来,就必须要让新的newHead的next指针指向上一个被插入的节点。
可上一个被插入的节点是什么?是原先的pcur?还是newTail?
是原先的pcur,但pcur已经遍历向下了,我们又去哪里找到原先的pcur呢?
这就又提醒我们,需要将每次插入节点的上一个节点保存下来(创建临时变量prev)。
回到再进行插入时,newHead变成了新的pcur,newHead->next变成prev,而prev被使用后,应该随着pcur向下走,也变成新的pcur,然后再让pcur遍历向下,这样prev和pcur就永远是前后关系。
代码实现:
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head)
{
ListNode* newTail = NULL;
ListNode* newHead = NULL;
ListNode* prev = head;
//存储老pcur,让newHead->next = prev
ListNode* pcur = head;
//遍历指针
ListNode* next = NULL;
//储存新pcur的next指针,让改动后,仍能让pcur = next
while (pcur)
{
//第一次头插
next = pcur->next;
// ListNode* next = head->next;
if (newHead == NULL)
{
newHead = newTail = pcur;
newTail->next = NULL;
// pcur = next;
}
//非空头插
else
{
newHead = pcur;
newHead->next = prev;
prev = pcur;
// pcur = next;
}
pcur = next;
}
return newHead;
}
虽然只有短短不到三十行代码量,但却想理清,也并不容易,我们来看几种常见错误。(实则就是我写的时候犯的诸多错误......)
错误呈现:①②③
①忘记要存pcur->next(没有创建临时指针变量next)
SLTNode* reverseList(SLTNode* head)
{
SLTNode* pcur = head;
SLTNode* newHead = NULL;
SLTNode* newTail = NULL;
SLTNode* prev = head;
while (pcur)
{
//头插
if (newHead == NULL)
{
newHead = newTail = pcur;
newTail->next = NULL;
pcur = pcur->next;
}
else
{
newHead = pcur;
newHead->next = prev;
prev = pcur;
pcur = pcur->next;
}
}
return newHead;
}

②在循环外写 ListNode* next = head->next;
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head)
{
ListNode* newTail = NULL;
ListNode* newHead = NULL;
ListNode* prev = head;
ListNode* pcur = head;
ListNode* next = head->next;
while (pcur)
{
//第一次头插
next = pcur->next;
if (newHead == NULL)
{
newHead = newTail = pcur;
newTail->next = NULL;
}
//非空头插
else
{
newHead = pcur;
newHead->next = prev;
prev = pcur;
}
pcur = next;
}
return newHead;
}
当原链表为空时,直接让next=head->next,就是对空指针进行访问,错误操作。
③在循环结束后才将newTail->next置空
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head)
{
ListNode* newTail = NULL;
ListNode* newHead = NULL;
ListNode* prev = head;
ListNode* pcur = head;
ListNode* next = NULL;
while (pcur)
{
//第一次头插
next = pcur->next;
if (newHead == NULL)
{
newHead = newTail = pcur;
}
//非空头插
else
{
newHead = pcur;
newHead->next = prev;
prev = pcur;
}
pcur = next;
}
newTail->next = NULL;
return newHead;
}
和第②的错误呈现的原因一致,当原链表为空时,不进入循环,newTail本身就为空指针,newTail->next的操作,同样是访问了空指针,错误操作。
思路③
------创建三个临时指针变量,遍历改变原链表的指针指向。

代码实现:
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
if(head == NULL)
return head;
ListNode* l1 = NULL;
ListNode* l2 = head;
ListNode* l3 = head->next;
ListNode* newHead = NULL;
ListNode* newTail = NULL;
while (l3)
{
l2->next = l1;
l1 = l2;
l2 = l3;
l3 = l3->next;
}
//漏了一个尾结点
l2->next = l1;
return l2;
}
思路详解:
与第二种思路相比,只是说没有把创建新头尾节点摆到明面上来说,但是思想是一致的,都是遍历原链表,改变各个节点的next指针的指向,从让原链表反着来。
创建三个临时指针变量l1、l2、l3,l1指向空,l2指向原链表的头结点,l3指向头结点的下一个节点,基本逻辑是让l2的next指针指向l1,再将l2赋给l1,再将l3赋给l2,最后让l3向后走,l3=l3->next,遍历完原链表,从而实现链表的反转。
细节答疑:
①为什么循环的条件是 l3 ?
先把循环条件当做l3,在循环的最后一轮,l1指向了倒数第二个节点,l2指向了尾结点,l3则是指向了NULL,如果此时循环还继续,那么l1会指向尾结点,l2会指向NULL,l3=l3->next,可是此时l3已经为空了,访问空指针,错误操作。
说明,当l3为空时,就应该要跳出循环。
②为什么要额外讨论空链表的情况?
在定义l3是,初始值为head->next,如果不额外讨论空链表的情况,又会出现访问空指针的操作错误。
③为什么在循环结束后,还会漏掉一个节点?
循环的最后一轮时,指向倒数第二个节点的l2的next指针指向了倒数第三个节点l1,然后,新的l1指向了倒数第二个节点,新的l2指向了尾结点,l3则是指向了NULL,我们还没来得及让新的l2->next指向新的l1(没来得及让尾结点的next指针指向倒数第一个节点),所以说漏了处理一个节点的问题。
让其l2->next = l1即可。
并且,l2指向的尾结点就是反转后链表的头结点,所以返回值是l2。
(3)合并两个有序链表
链接:合并两个有序链表
思路①
------创建新链表,设立新的头尾指针newHead,newTail,进行遍历比较,尾插组成新链表。
尾插的关键在于第一要分空链表和非空链表的情况,第二要先让原尾结点的next指针指向新的插入节点之后,再调整newTail的指向(不然无法使尾插的节点相连构成新链表)
if(list1 == NULL)
return list2;
if(list2 == NULL)
return list1;
ListNode* newHead = NULL;
ListNode* newTail = NULL;
ListNode* l1 = list1;
ListNode* l2 = list2;
while(l1 != NULL && l2 != NULL)
{
if(l1->val < l2->val )
//头插
{
if( newHead == NULL)
{
newHead = newTail = l1;
// newTail->next = NULL;
}
else
{
newTail->next = l1;
newTail = l1;
}
l1 = l1->next;
}
else
{
if( newHead == NULL)
{
newHead = newTail = l2;
// newTail->next = NULL;
}
else
{
newTail->next = l2;
newTail = l2;
}
l2 = l2->next;
}
}
//l1 == NULL l2 == NULL
if(l1 == NULL)
{
newTail->next = l2;
}
// else 可行
if(l2 == NULL)
{
newTail->next = l1;
}
return newHead;
错误呈现:
①当一个链表为空返回另一个链表的头指针的逻辑,不是if.....else......。
②不要在头插时,让newTail->next置空,会改变l1或是l2的next指针指向,丢失了后面紧跟着的数据。
思路②
(4)寻找链表的中间节点
链接:寻找链表的中间节点
思路①(经典思路)
------快慢指针,2*slow = fast,当fast走到尾,slow就找到了中间节点。
注意:寻找偶数个节点的链表的中间节点,指的是找中间两个的后一个节点。
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
//快慢指针
ListNode* fast = head;
ListNode* slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
//slow就能找到中间节点
return slow;
}
(5)环形链表的约瑟夫问题
链接:环形链表的约瑟夫问题
(6)分割链表
链接:分割链表
------未完待续------