目录
思路2:创建新链表,将原链表中值不为val的结点依次尾插入新链表
单链表oj
一、双指针
1.移除元素
思路:定义两个值src、dst“守在”数组的首元素,dst称为“驻守指针”,src称为“探路指针”,开始while循环遍历数组,若src对应的值不等于val,则将src对应的值赋给dst,然后src++,dst++,若src对应的值等于val则不做赋值操作,直接src++,dst不动,由于两种情况都需要src++,因此可以提出src++并把else去掉,直接把提出来的src++写在外面即可。当src遍历完成后跳出循环(dst不可能优先于src结束遍历),此时dst就是nums数组中不等于val的元素数量,直接返回dst即可。(时间复杂度O(n),空间复杂度O(1))
int removeElement(int* nums, int numsSize, int val)
{
int src = 0 ,dst = 0;
while(src < numsSize)
{
if(nums[src] != val)
{
nums[dst] = nums[src];
dst++;
}
src++;
}
return dst;
}
2.删除有序数组中的重复项
思路:同样定义两个元素src、dst,不同的是,由于要删除数组中的重复项,那么src的位置要放在dst的下一位,这样二者才可以比较,同样进入while循环src开始遍历。当src对应的值与dst对应的值不同时,先dst++,再把src的值赋给dst,然后src++(为什么要先dst++,因为既然src往前走了,那么dst前面的一个数肯定与自身对应的数相等,因此先dst++然后用src对应的值来覆盖这个重复的值) ,但难免会出现dst++后所在的位置与src的位置重合,从而出现“自己赋值给自己”的多余操作的情况,那么此时只需要再加上一条if条件语句即可,如果src != dst再赋值,相等则不用赋值,直接src++即可;而遍历的过程中如果src与dst对应的值相等时,则只需要src++,不需要赋值操作。src遍历结束后跳出循环,此时nums中唯一元素的个数为dst+1,返回这个值即可。(时间复杂度O(n),空间复杂度O(1))
int removeDuplicates(int* nums, int numsSize)
{
int dst = 0,src = dst+1;
while(src < numsSize)
{
if(nums[src] != nums[dst])
{
dst++;
if(dst!=src)
nums[dst] = nums[src];
src++;
}
else{
src++;
}
}
return dst+1;
}
可以发现这段代码还有简化的空间,第一个if语句中先dst++再嵌套一个if (dst != src),可以把++放在if表达式中dst的前面,即if(++dst != src);同时可以看到,第一个if语句中有src++,else语句中也有src++,则可以同时提出src++,去掉else语句,单独写在外面即可,如下:
int removeDuplicates(int* nums, int numsSize)
{
int dst = 0,src = dst+1;
while(src < numsSize)
{
if(nums[src] != nums[dst])
{
if(++dst!=src)
nums[dst] = nums[src];
}
src++;
}
return dst+1;
}
此时又发现嵌套进去的if语句前面空空如也,那是不是就可以与第一个if语句的表达式合并起来了?那当然没问题了,再次简化的代码如下:
int removeDuplicates(int* nums, int numsSize)
{
int dst = 0,src = dst+1;
while(src < numsSize)
{
if(nums[src] != nums[dst] && ++dst!=src)
{
nums[dst] = nums[src];
}
src++;
}
return dst+1;
}
二、三指针
1.反转单链表
思路:此题可以有好几种解法,可以创建一个新的空链表,然后遍历原链表依次头插到新链表中,最后返回新链表的头结点即可。这里重点讲三指针的解题思路,首先考虑需要反转的链表是否为空,如果链表为空则只需要返回head即可;若不为空,创建三个结构体指针,分别命名为n1、n2、n3,第一个指针n1指向NIULL,第二个指针n2指向链表头结点地址head,第三个指针n3指向n2的下一个结点n2->next,三个指针同时遍历链表,循环条件为指向头结点地址的n2不为空,遍历过程中,进行n2->next = n1的操作(反转操作),然后再继续往后遍历,n1 = n2 ,n2 = n3, n3 = n3->next ,由于n3在n2的后面,必然先于n2成为空指针,但是此时n2还不为空,循环还在继续,为了避免出现n3的空指针异常,需要在n3 = n3->next前添加一个条件语句if(n3 != NULL)或if(n3),如此往后遍历一次后,重复n2->next = n1的操作,直到n2为空跳出循环,便完成了链表的反转,此时n1成为链表新的头结点,返回n1即可。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
if(head == NULL)
{
return head;
}
ListNode* n1, *n2, *n3;
n1 = NULL;
n2 = head;
n3 = n2->next;
while(n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if(n3)
{
n3 = n3->next;
}
}
return n1;
}
三、快慢指针
1.链表的中间结点
思路:定义快慢指针 fast、slow,均指向头结点,两个指针同时遍历链表,不同的是,之所以称为快慢指针,是因为它们遍历的速度不一样,在这道题里面要求返回链表中间结点,因此遍历时慢指针往后走一步,快指针则需要往后走两步,当快指针遍历结束跳出循环后,此时慢指针是不是已经走一半了指向链表的中间结点了?答案是的,出循环后直接返回slow就大功告成了。这里说一下循环的条件,当链表有偶数个结点时,就有两个中间结点,而当fast为空时,此时slow正好指向了题目要求返回的第二个中间结点,此时则跳出循环,因此第一个循环条件时fast不为空;当链表中有奇数个结点时,fast指向尾结点时,slow刚好指向中间结点,此时也要跳出循环,因此第二个循环条件时fast->next不为空,注意,第一第二个循环条件不可互换!
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
//定义快慢指针
ListNode* slow, *fast;
slow = fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
2.环形链表
思路:定义快慢指针 fast、slow指向头结点,往后遍历,如果有环,那么fast指针一定先进入环内,等到slow入环时,两者如果刚好相遇,那么就证明有环然后返回true,如果还没有相遇,此时fast与slow相距一定的距离,令快指针一次走两步,慢指针一次走一步,那么就意味着快指针每次接近慢指针一步,因此总会有相遇的时候,如果相遇了则返回true,总而言之,如果链表带环,快慢指针必有相遇之时。如果快慢指针不相遇,即快指针某一刻为空了,那就证明链表不带环,返回false。
思考:慢指针走一步的时候,快指针一定要走两步吗?走3、4、5、6....步行不行?事实上,快指针无论走多少步都满足在带环链表中相遇,但是在编写代码的时候会有额外的步骤引入,涉及到快慢指针的算法题中通常使用慢指针走一步快指针走两步的方式。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) {
//定义快慢指针
ListNode* fast, *slow;
fast = slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(fast == slow)
{
//相遇必有环
return true;
}
}
//循环结束此时fast为空,证明链表不带环
return false;
}
3.环形链表Ⅱ
思路: 同样定义快慢指针 fast、slow指向头结点,开始遍历找到他们相遇的点,如果不相遇则链表不带环,返回NULL,如果相遇了,在这里记住一个结论:快慢指针相遇点与头结点到入环起始结点的距离是相等的,因此找到快慢指针相遇点后,定义一个结构体指针pcur指向链表的头结点,此时pcur与慢指针slow同时往前走,它们两个的相遇点便是链表开始入环的第一个结点,直接返回即可。(对于这个结论可自行画图验证一下,无论链表多长环有多大结论都成立,需要一定的数学证明这里不做介绍)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode *detectCycle(struct ListNode *head) {
//快慢指针
ListNode* fast, *slow;
fast = slow = head;
while(fast && fast->next)
{
//找相遇点
slow = slow->next;
fast = fast->next->next;
if(fast == slow)
{
//相遇点与头结点到入环第一个结点的距离相同
ListNode* pcur = head;
while(pcur != slow)
{
pcur = pcur->next;
slow = slow->next;
}
return pcur;
}
}
return NULL;
}
四、快慢指针+三指针
1.链表的回文结构
思路1: 转换数组比较法
这是一个比较投机取巧的方法,因为题目已经告诉了我们链表长度最大不超过900,因此我们可以创建一个空间为900的整型静态数组,然后定义一个结构体指针指向链表头结点开始遍历链表,将链表的值全部搬入数组当中,如此数组就可以从两头向中间遍历依次比较元素从而判断是否回文结构了。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
bool chkPalindrome(ListNode* A) {
//创建长度900的数组
int arr[900] = {0};
ListNode* pcur = A;
int i = 0;
while(pcur)
{
arr[i] = pcur->val;
i++;
pcur = pcur->next;
}
int left = 0;
int right = i-1;
while(left<right)
{
if(arr[left] != arr[right])
{
return false;
}
left++;
right--;
}
return true;
}
};
思路2:快慢指针与三指针并用法
先使用快慢指针找到链表的中间结点,再用三指针将以中间结点作为头结点的新链表进行反转,如此便可以从两头开始遍历依次比较结点元素判断是否回文结构了。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
//找中间结点
ListNode* MiddleNode(ListNode* head)
{
ListNode*fast, *slow;
fast = slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
//反转链表
ListNode* reverseList(ListNode* head)
{
if(head == NULL)
{
return head;
}
ListNode* n1, *n2, *n3;
n1 = NULL;
n2 = head;
n3 = n2->next;
while(n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if(n3)
{
n3 = n3->next;
}
}
return n1;
}
bool chkPalindrome(ListNode* A) {
//找中间结点
ListNode* mid = MiddleNode(A);
//反转中间结点之后的链表并返回新链表的头结点
ListNode* right = reverseList(mid);
//同时遍历原链表和反转后的链表
ListNode* left = A;
while(right)
{
if(left->val != right->val)
{
return false;
}
left = left->next;
right = right->next;
}
return true;
}
};
五、其他
1.合并两个有序链表
思路1:创建空链表
具体代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
//考虑list1或者list2为空的情况
if(list1 == NULL)
{
return list2;
}
if(list2 == NULL)
{
return list1;
}
//创建空链表
ListNode*newHead,*newTail;
newHead = newTail = NULL;
ListNode* L1 = list1;
ListNode* L2 = list2;
//同时遍历两个链表并比较
while(L1 && L2)
{
if(L1->val < L2->val)
{
if(newHead == NULL)
{
//L1直接插入到空链表中
newHead = newTail = L1;
}
else
{
//链表不为空,则依次尾插
newTail->next = L1;
newTail = newTail->next;
}
L1 = L1->next;
}
else
{
if(newHead == NULL)
{
//L2直接插入到空链表中
newHead = newTail = L2;
}
else
{
//链表不为空,则依次尾插
newTail->next = L2;
newTail = newTail->next;
}
L2 = L2->next;
}
//当L1未完全遍历
if(L1)
{
newTail->next = L1;
}
//L2未完全遍历
if(L2)
{
newTail->next = L2;
}
}
return newHead;
}
可以看出这样设计if判断并插入的代码,每次循环比较结束后都需要判断newTail是否为空来决定如何插入,会出现代码冗余的情况。那该如何解决?这就可以使用第二个思路,创建一个非空的带头链表。
思路2:创建非空链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
//考虑list1或者list2为空的情况
if(list1 == NULL)
{
return list2;
}
if(list2 == NULL)
{
return list1;
}
//创建非空链表
ListNode* newHead,* newTail;
//在链表申请一个结点的空间占位子
//带头链表,占位子的结点称为“哨兵位”
newHead = newTail = (ListNode*)malloc(sizeof(ListNode));
ListNode* L1 = list1;
ListNode* L2 = list2;
//同时遍历两个链表并比较
while(L1 && L2)
{
if(L1->val < L2->val)
{
//链表不为空,依次尾插
newTail->next = L1;
newTail = newTail->next;
L1 = L1->next;
}
else
{
//链表不为空,则依次尾插
newTail->next = L2;
newTail = newTail->next;
L2 = L2->next;
}
//当L1未完全遍历
if(L1)
{
newTail->next = L1;
}
//L2未完全遍历
if(L2)
{
newTail->next = L2;
}
}
//需要返回链表中第一个有效结点
//即返回“哨兵位”的下一个结点
ListNode* retHead = newHead->next;
//别忘了free掉newHead并置空
free(newHead);
newHead = NULL;
return retHead;
}
如此便解决了代码冗余的问题,申请一个头结点空间,遍历两个链表直接从小到大往后插入即可,就不需要考虑链表为空的情况,插入结束后要记得把申请的空间释放。
2.移除链表元素
思路1: 遍历链表对值为val的结点执行删除操作
整体思路是先定义一个结构体指针pcur表示头结点地址开始从前往后遍历,同时定义一个函数SLTEraseAfter,用于删除指定位置之后的结点,如果pcur->next->val ==val,则将pcur传入函数中进行删除操作,循环条件是pcur以及pcur->next均不为空,遍历结束后跳出循环。但是这样做我们得单独考虑并解决两个问题:1.链表为空——运用if条件语句表示链表为空时,直接返回head即可;2.链表头结点的值为val——运用while循环,循环条件head不为空,且head->val == val,此时只需要将head往后移一位,即head = head->next,再把前面的结点free掉即可,如果移动后head的值依旧为val,则继续循环重复以上操作。最后,遍历完成整个数组后,将head返回。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
void SLTEraseAfter(ListNode* pos)
{
//pos pos->next pos->next->next
//暂存到del防止丢失
ListNode* del = pos->next;
//pos del del->next
pos->next = del->next;
free(del);
del = NULL;
}
struct ListNode* removeElements(struct ListNode* head, int val) {
//考虑链表为空的情况
if(head == NULL)
{
return head;
}
//处理头结点值等于val的情况
while(head && head->val == val)
{
ListNode* tmp = head;
head = head->next;
free(tmp);
tmp = NULL;
}
ListNode* pcur = head;
while(pcur!=NULL && pcur->next!=NULL)
{
if(pcur->next->val == val)
{
SLTEraseAfter(pcur);
}
else{
pcur = pcur->next;
}
}
return head;
}
思路2:创建新链表,将原链表中值不为val的结点依次尾插入新链表
定义新链表的首尾结点分别为newHead、newTail并同时置空,同时定义一个结构体指针pcur指向头结点,如果pcur不为空则开始循环遍历,将值不为val的结点尾插到新链表中,需要注意的是要先判断新链表是否为空,若为空则直接newHead = newTail = pcur,若不为空则尾插到newTail的后面并更新newTail的位置。遍历结束跳出循环后还需要进行newTail指向空指针的操作,但还需要考虑的是newTail为空的情况,因此还需要一个if条件语句进行判断,以免出现空指针异常,最后返回newHead。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
//创建空链表
ListNode* newHead, *newTail;
newHead = newTail =NULL;
ListNode* pcur = head;
while(pcur)
{
//把值不为val的结点尾插到新链表中
if(pcur->val != val)
{
//链表为空
if(newTail == NULL)
{
newHead = newTail = pcur;
}
//链表非空
else{
newTail->next = pcur;
//更新newTail的位置
newTail = newTail->next;
}
}
pcur = pcur->next;
}
if(newTail)
{
newTail->next = NULL;
}
return newHead;
}
3.相交链表
思路:同时遍历两个链表,只要出现两个链表存在地址相同的结点便证明两链表相交了,只需要返回第一个地址相同的结点即可,如果没有,就返回NULL。可事实真这么简单吗?不完全是,这只适用于两链表长度相同的情况,如果不相同就不成立。那如果不相同该怎么做?先求两个链表的长度,然后计算长度差,假设为gap,那么就可以让长的链表单独往后走gap步,那么两个链表就处于“同一起跑线”上了,那么就可以用前面的思路同时遍历然后返回第一个地址相同的结点。注意: 不能是相同的“值”,而是“址”!
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
ListNode* pa = headA;
ListNode* pb = headB;
int sizeA = 0, sizeB = 0;
//求两个链表长度
while(pa)
{
sizeA++;
pa = pa->next;
}
while(pb)
{
sizeB++;
pb = pb->next;
}
//计算长度差的绝对值
int gap = abs(sizeA - sizeB);
ListNode* shortList = headA;
ListNode* longList = headB;
if(sizeA > sizeB)
{
longList = headA;
shortList = headB;
}
//让长链表先走gap步
while(gap--)
{
longList = longList->next;
}
//longList和shortList在同一起跑线
while(shortList)
{
if(shortList == longList)
{
return longList;
}
shortList = shortList->next;
longList = longList->next;
}
return NULL;
}