在下面的所有题目中,链表都是单链表,且链表节点的声明如下:
struct ListNode {
int val;
struct ListNode *next;
};
文章目录
移除链表元素
题目描述
思路分析
大眼一看这题很简单,
无非是遍历一遍链表,
把与 val 相同的节点删除掉,
细节就是删掉这个节点后还要把前后两个节点连接起来。
那么就要同时找到前后两个节点。
此时会有两种情况:
-
这是最基本的一种情况,此时只需要再用一个指针 prev 标记前一个节点即可,当遍历到需要删除的节点时,用 next 记住它下一个位置,然后连接 prev 和 next,释放当前节点,从 next 节点继续走。
-
当链表第一个节点就需要删除时,prev 还是个空指针,此时直接动头指针 head ,先标记下一个,然后释放掉头节点,然后 head 指向下一个。即便链表所有节点都需要删除也没问题。
代码实现
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* prev = NULL;
while(cur)
{
//头节点不需要删除时
if(head->val != val)
{
struct ListNode* next = cur->next;
//当前节点不需要删除时
if(cur->val != val)
{
prev = cur;
cur = next;
}
//当前节点需要删除时
else
{
prev->next = next;
free(cur);
cur = next;
}
}
//头节点需要删除时
else
{
struct ListNode* next = cur->next;
free(cur);
head = next;
cur = next;
}
}
return head;
}
反转链表
题目描述
思路分析
这里提供三种思路:
- 逆置 val;
- 翻转 next,即让 next 指向前一个而非下一个;
- 依次取最后一个节点进行头插。
下面只讲解第三种思路:
依次取最后一个节点进行头插的话,实际执行起来效率是很低的,因为每次找到最后一个节点都要遍历。
所以不妨转化思路,每次取第一个节点进行尾插,而为了避免遍历,每次取下来头结点之后插入到一个新链表中:
代码实现
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* newhead = NULL;
struct ListNode* cur = head;
while(cur)
{
struct ListNode* next = cur->next;
cur->next = newhead;
newhead = cur;
cur = next;
}
return newhead;
}
链表的中间结点
题目描述
思路分析
这是一个经典的快慢指针问题。
定义两个指针 slow 和 fast ,
slow 每次走一步, fast 每次走两步,
当 fast 走到头,slow 也就走到了中间结点。
不过节点个数为奇数偶数时终止条件还不太一样:
代码实现
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* slow = head;
struct ListNode* fast = head;
while(fast != NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
链表中倒数第k个节点
题目描述
思路分析
假想最后一个节点指向一个空节点,
那么倒数第 k 个节点与该空节点正好隔着 k 个节点。
所以如果能找到两个节点,
让这两个节点之间始终隔着 k 个节点,
当前一个节点走到空时,
返回后一个节点即可。
代码实现
struct ListNode* getKthFromEnd(struct ListNode* head, int k)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
for (int i=0; i<k; i++)
fast = fast->next;
while (fast != NULL)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
合并两个有序链表
题目描述
思路分析
这道题的思路很明了,
遍历两个链表,
每次取下小的那个节点尾插到新节点。
但这就有几种情况很棘手:
- 两个链表中有空链表,此时直接返回非空的即可,都为空则随便返回。
- 其中一个链表走到最后了,但另一个链表还没有,此时将未走完的那个链表接到新链表后面即可。
代码实现
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if(list1 == NULL)
return list2;
if(list2 == NULL)
return list1;
struct ListNode* list3 = NULL;
struct ListNode* phead = NULL;
//先创建新链表的头节点
if (list1->val < list2->val)
{
phead = list1;
list1 = list1->next;
}
else
{
phead = list2;
list2 = list2->next;
}
list3 = phead;
/*上面代码如果觉得麻烦的话可以选择创建一个哨兵位,但最后别忘了释放掉。
struct ListNode* list3 = NULL;
struct ListNode* head = NULL;
list3 = head = (struct ListNode*)malloc(sizeof(struct ListNode));
list3->next = NULL;
*/
//遍历两个链表
while(list1 && list2)
{
if (list1->val < list2->val)
{
list3->next = list1;
list1 = list1->next;
list3 = list3->next;
}
else
{
list3->next = list2;
list2 = list2->next;
list3 = list3->next;
}
}
//其中一个链表走完
if(list1)
list3->next = list1;
if(list2)
list3->next = list2;
return phead;
}
分割链表
题目描述
现有一链表的头指针 ListNode pHead*,给一定值 x,编写一段代码将所有小于 x 的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
思路分析
这道题最恶心的地方在于不改变原来数据的相对顺序。
那么这里提供一个很妙的思路。
将小于 x 的节点拿出来组成一个新链表,
将大于 x 的节点拿出来组成一个新链表,
再把两个链表连接起来返回即可。
代码实现
ListNode* partition(ListNode* pHead, int x)
{
//创建大于x的链表
struct ListNode* greaterList = NULL;
struct ListNode* greaterTail = NULL;
greaterList = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));
greaterList->next = greaterTail->next = NULL;
//创建小于x的链表
struct ListNode* lessList = NULL;
struct ListNode* lessTail = NULL;
lessList = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode));
lessList->next = lessTail->next = NULL;
while(pHead)
{
//节点的值小于x时
if(pHead->val < x)
{
lessTail->next = pHead;
lessTail = lessTail->next;
}
//节点的值大于x时
else
{
greaterTail->next = pHead;
greaterTail = greaterTail->next;
}
//找到下一个节点,继续循环
pHead = pHead->next;
}
//连接两个链表
lessTail->next = greaterList->next;
greaterTail->next = NULL;
pHead = lessList->next;
//释放分配的空间
free(greaterList);
free(lessList);
return pHead;
}
回文链表
题目描述
思路分析
判断链表的回文结构是不能和判断回文字符串一概而论的,
因为链表不能从后向前遍历。
所以这里的思路是先找到链表的中间结点,
然后翻转后半部分,
一个从头开始,一个从中间开始进行比较。
前两步处理都是前面做过的,
所以这道题是一道裁缝题(doge。
代码实现
bool isPalindrome(struct ListNode* head){
//先找到中间结点
struct ListNode* mid = head;
struct ListNode* tmp = head;
while(tmp && tmp->next)
{
tmp = tmp->next->next;
mid = mid->next;
}
//翻转后半部分
struct ListNode* newhead = NULL;
while (mid)
{
struct ListNode* next = mid->next;
mid->next = newhead;
newhead = mid;
mid = next;
}
//比较
while(head && newhead)
{
if(head->val != newhead->val)
return false;
head = head->next;
newhead = newhead->next;
}
return true;
}
相交链表
题目描述
思路分析
题目要求不能破坏链表的结构,
如果不限制这一点的话,
有一个简单暴力的方法:
先遍历一个链表,然后将每个节点都置为不可能出现的数值,再遍历另一个链表,如果有节点匹配上了那个超出数据范围的值,那么两个链表就相交,返回第一个匹配上的节点即可。
下面进行正经分析:
两个链表长度相同的话好说,同步遍历两个链表,当走到的节点位置相同时就返回该节点。
而两个链表长度不相同的话,如果能统一两个链表的起点,然后重复上面的步骤同样可以,所以现在问题就来到了怎么统一两个链表的起点。
首先遍历两个链表统计出两个链表的长度。此时可以先比较一下两个链表的尾结点,如果尾结点的地址不一致的话直接返回 false。
然后动较长的那个链表,使它比较的起始位置向前移动,移动的次数为两个链表的长度差值。
同步之后开始比较即可。
代码实现
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
if(headA == NULL || headB == NULL)
return NULL;
//找到两个单链表的最后一个节点并比较
//同时统计两个单链表的长度
int cntA = 0;
int cntB = 0;
struct ListNode *curA = headA, *curB = headB;
while (headA->next)
{
headA = headA->next;
cntA++;
}
while (headB->next)
{
headB = headB->next;
cntB++;
}
if (headA != headB)
return NULL;
//统一两个单链表的两个起点
if (cntA > cntB)
for (int i=0; i<cntA-cntB; i++)
curA = curA->next;
else if (cntA < cntB)
for (int i=0; i<cntB-cntA; i++)
curB = curB->next;
//比较
struct ListNode* tmpA = curA;
struct ListNode* tmpB = curB;
while(tmpA != tmpB && tmpA && tmpB)
{
tmpA = tmpA->next;
tmpB = tmpB->next;
}
return tmpA;
}
环形链表
题目描述
思路分析
由于这道题没有限制不能改变链表结构,
所以上道题提到的标记法是可行的:
遍历链表,每到一个节点就将该节点存储的值改为数据范围之外的值,如果链表中有环的话一定会再走到值为数据范围之外的节点,该节点就是成环的第一个节点。
当然,如果用这个思路这道题就没意思了。
下面是正经思路:
还是采用快慢指针的方法,快指针一次走两个,慢指针一次走一个,如果链表中有环的话,两个指针一定会相交,证明如图:
代码实现
bool hasCycle(struct ListNode *head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
return true;
}
return false;
}
环形链表II
题目描述
思路分析
上一道题的升级版,
如果用暴力标记的话很容易就解决这道题,
但不是道题的灵魂所在。
下面是正经分析:
首先还是先验证链表是否有环,所以重复上一道题中移动快慢指针这一步骤,如果 fast 和 slow 相遇的话,让一个指针从头开始,另一个指针从相遇点开始,两个指针同步前进,则两个指针一定会相遇,且该相遇点就是环开始的节点。证明如下:
代码实现
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode *fast = head, *slow = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
{
struct ListNode *meet = fast;
while (meet != head)
{
meet = meet->next;
head = head->next;
}
return meet;
}
}
return NULL;
}
复制带随机指针的链表
题目描述
思路分析
这题挺复杂的。
你可能上来想先复制出来每个节点,
但 random 怎么办呢?
如果通过 val 去找 random 的指向时,
val 万一有重复呢?
所以复制一个链表单从它本身是做不到复制 random 的指向的。
那么就要借助原链表。
在原链表每个节点后面复制该节点:
将复制的节点插入到原链表两两之间:
用一个 cur 从头开始遍历,
cur 的 next 的 random 就是对应拷贝节点的 random,
cur 的 random 的 next 就是拷贝节点的 random,
如果 cur 的 random 不为 NULL ,
就连接 cur->next->random 和 cur->random->next,
这样就完成了第一个拷贝节点的 random 的拷贝,
然后继续向下迭代即可 (cur=cur->next->next)。
random 处理完成后再断开节点重新连接即可。
代码实现
struct Node* copyRandomList(struct Node* head)
{
if (head == NULL)
return NULL;
//插入拷贝节点
struct Node* cur = head;
while (cur)
{
struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
newnode->next = cur->next;
newnode->val = cur->val;
newnode->random = NULL; //random先置空,方便后面不用处理
cur->next = newnode;
cur = newnode->next;
}
//处理拷贝节点的random
cur = head;
while (cur)
{
struct Node* tmp = cur->random;
if (tmp)
cur->next->random = tmp->next;
cur = cur->next->next;
}
//断开节点
cur = head;
struct Node* ret = cur->next;
while (cur)
{
struct Node* copy = cur->next;
struct Node* next = copy->next;
cur->next = next;
if (next)
copy->next = next->next;
cur = next;
}
return ret;
}
对链表进行插入排序
题目描述
思路分析
插入排序,可以创建一个新链表,
新链表的头 SortHead 指向原链表的头,
然后开始遍历原链表,
拿遍历到的节点与新链表中的每个节点进行比较,
直到找到合适的位置,然后插入。
插入的话有两种情况,
一种是上来就是最小的,直接头插,
另一种就是往里走一段再插,包含尾插和中间插。
如果是中间插则需要时刻记录前一个节点的地址,
下面用 p 来记录;
而如果尾插的话,p 实际指向最后一个节点,
就相当于在 p 和后面的空节点之间插入,
可以一块处理。
代码实现
struct ListNode* insertionSortList(struct ListNode* head)
{
if (head == NULL || head->next == NULL)
return head;
struct ListNode* cur = head->next;
struct ListNode* SortHead = head;
SortHead->next = NULL;
while (cur)
{
struct ListNode* next = cur->next;
struct ListNode* p = NULL;
struct ListNode* c = SortHead;
while (c)
{
if (c->val > cur->val)
break;
else
{
p = c;
c = c->next;
}
}
//头插
if (p == NULL)
{
cur->next = c;
SortHead = cur;
}
//尾插或中间插
else
{
p->next = cur;
cur->next = c;
}
cur = next;
}
return SortHead;
}
删除排序链表中的重复元素II
题目描述
思路分析
这道题比较麻烦。
就拿上图举例:
为了删除所有的 3 ,
我要同时知道 2 和 4 两个节点的地址,
然后连接 2 和 4 。
继续删除 4 ,那就要知道 2 和 5 的地址,
然后连接 2 和 5 。
此时就需要三个指针:cur、prev 和 next 。
其中要比较 cur->cal 和 next->val ,
如果二者相同,则 next 继续走,cur 不动,
直到 cur 走到头或二者不相等,
然后连接 prev 和 next ,继续进行迭代。
但是,如果 prev 为空呢?就如下面这种情况:
其实这种情况也好处理,
直接改变链表的头 head 就好了,
让 head 指向 next。
代码实现
struct ListNode* deleteDuplicates(struct ListNode* head)
{
if (head == NULL || head->next == NULL)
return head;
struct ListNode *prev = NULL, *cur = head, *next = head->next;
//开始迭代
while (next)
{
//遇到相同节点
if (cur->val == next->val)
{
next = next->next;
//next走到空时
if (next == NULL)
{
//prev为空时
if (prev == NULL)
head = next;
//prev不为空时
else
prev->next = next;
return head;
}
//next还没走到空时
if (cur->val != next->val)
{
//prev为空时
if (prev == NULL)
head = next;
//prev不为空时
else
prev->next = next;
cur = next;
next = next->next;
}
}
//继续迭代
else
{
prev = cur;
cur = next;
next = next->next;
}
}
return head;
}