目录
前言
今天我们的主题是实现单向不带头不循环的单链表
链表的实现
接口的声明
typedef int SLTDataTypde;
typedef struct SLTNode
{
SLTDataTypde data;
struct SLTNode *next;
}SLTNode;
//链表销毁
void SLTNodeDestor(SLTNode *ps);
//创建结点
SLTNode *BuySLTNode(int x);
//打印
void SLTprint(SLTNode *ps);
//尾插
void SLTPushBack(SLTNode **ps, int x);
//头插
void SLTPushFront(SLTNode **ps,int x);
//头删
void SLTPopFront(SLTNode **ps);
//尾删
void SLTPopBack(SLTNode **ps);
//查找
SLTNode* SLTFind(SLTNode *ps, int x);
//指定位置后插入
void SLTInsertAfter(SLTNode *pos, int x);
//指定位置前插入
void SLTInsertBefor(SLTNode **ps, SLTNode *pos, int x);
//移除指定位置之后
void SLTEraseAfter(SLTNode *pos);
//移除指定位置之前
void SLTEraseBefor(SLTNode **ps ,SLTNode *pos);
接口的实现
#include"SLTNode.h"
//创建结点
SLTNode *BuySLTNode(int x)
{
SLTNode *tmp = (SLTNode *)malloc(sizeof(SLTNode));
if (!tmp)
{
perror("BuySLTNode::malloc");
exit(1);
}
tmp->data = x;
tmp->next = NULL;
return tmp;
}
//打印
void SLTprint(SLTNode *ps)
{
SLTNode *cur = ps;
while (cur != NULL)
{
printf("%d->",cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//链表销毁
void SLTNodeDestor(SLTNode *ps)
{
assert(ps);
while (ps)
{
SLTNode *del = ps;
ps = ps->next;
free(ps);
}
}
//尾插
void SLTPushBack(SLTNode **ps, int x)
{
SLTNode *newNode = BuySLTNode(x);
if (*ps == NULL)
{
*ps = newNode;
}
else
{
SLTNode *tail = *ps;
//找尾
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newNode;
}
}
//头插
void SLTPushFront(SLTNode **ps,int x)
{
SLTNode *newNode = BuySLTNode(x);
newNode->next = *ps;
*ps = newNode;
}
//头删
void SLTPopFront(SLTNode **ps)
{
if (*ps == NULL)
return;
SLTNode *delNode = *ps;
*ps = delNode->next;
free(delNode);
delNode = NULL;
}
//尾删
void SLTPopBack(SLTNode **ps)
{
SLTNode *phead = *ps;
//只有一个结点
if (phead->next == NULL)
{
free(phead);
*ps = NULL;
phead = NULL;
}
else if (*ps == NULL)
{
return ;
}
else
{
SLTNode *cur = *ps;
SLTNode *prev = NULL;
//剩多个结点
while (cur->next != NULL)
{
prev = cur;
cur = cur->next;
}
free(cur);
cur = NULL;
prev->next = NULL;
}
}
//查找
SLTNode* SLTFind(SLTNode *ps, int x)
{
if (ps == NULL)
return NULL;
SLTNode *cur = ps;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//指定位置后插入
void SLTInsertAfter(SLTNode *pos, int x)
{
assert(pos);
SLTNode *next = pos->next;
SLTNode *newNode = BuySLTNode(x);
pos->next = newNode;
newNode->next = next;
}
//指定位置前插入
void SLTInsertBefor(SLTNode **ps, SLTNode *pos, int x)
{
assert(pos);
SLTNode *newNode = BuySLTNode(x);
if (pos == *ps)
{
newNode->next = *ps;
*ps = newNode;
}
else
{
SLTNode *cur = *ps;
SLTNode *prev = NULL;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = newNode;
newNode->next = cur;
}
}
//移除指定位置之后
void SLTEraseAfter(SLTNode *pos)
{
assert(pos);
SLTNode *next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
//移除指定位置
void SLTEraseBefor(SLTNode ** ps, SLTNode *pos)
{
assert(pos);
SLTNode *next = pos->next;
if (*ps == pos)
{
free(*ps);
*ps = NULL;
*ps = next;
}
else
{
SLTNode *prev = NULL;
SLTNode *cur = *ps;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = cur->next;
free(cur);
cur = NULL;
}
}
销毁
//链表销毁
void SLTNodeDestor(SLTNode *ps)
{
assert(ps);
while (ps)
{
SLTNode *del = ps;
ps = ps->next;
free(ps);
}
}
打印
遍历链表的每个结点,打印这个结点的data
//打印
void SLTprint(SLTNode *ps)
{
SLTNode *cur = ps;
while (cur != NULL)
{
printf("%d->",cur->data);
cur = cur->next;
}
printf("NULL\n");
}
创建结点
//创建结点
SLTNode *BuySLTNode(int x)
{
SLTNode *tmp = (SLTNode *)malloc(sizeof(SLTNode));
if (!tmp)
{
perror("BuySLTNode::malloc");
exit(1);
}
tmp->data = x;
tmp->next = NULL;
return tmp;
}
尾插
创建一个新的结点newNode,找到最后一个结点的位置tail,用tail链接newNode,这里要注意的是我们的链表的实现是以不带头为目的的,所以会是一个空表,如果是空表的情况下,需要将ps初始化成为第一个结点,往后尾插就只需要找尾了,这里要传二级指针,为了改变外部的指针
//尾插
void SLTPushBack(SLTNode **ps,int x)
{
SLTNode *newNode = BuySLTNode(x);
//如果是空表
if (*ps == NULL)
{
*ps = newNode;
}
else
{
//尾插找尾
SLTNode *tail = *ps;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newNode;
}
}
传递一级指针无法改变main函数的plist的指针,因为是在不同的栈帧中开辟的,只是一份值拷贝,互不影响,所以传递二级指针
头插
头插只需要创建一个新的结点newNode,让新的结点链接ps,再让ps去做新的头,由于需要改变外面的指针,还是要传递二级指针,头插即使是面对链表为空还是不会受影响
//头插
void SLTPushFront(SLTNode **ps,int x)
{
SLTNode *newNode = BuySLTNode(x);
newNode->next = ps;
//ps去做新的头
*ps = newNode;
}
头删
头删同样需要改变外面的指针,所以要传二级,移除第一个结点,让第二个结点做新的头
//头删
void SLTPopFront(SLTNode **ps)
{
if (*ps == NULL)
return;
SLTNode *delNode = *ps;
*ps = delNode->next;
free(delNode);
delNode = NULL;
}
尾删
尾删需要考虑三种情况
1、表为空,不需要删除,直接返回
2、只剩一个结点,删除这个结点,再置空,也需要改变外面指针的指向,必须传二级指针
3、有多个结点,定义前驱指针prev,和目标指针cur,当cur指向最后一个结点的时候释放cur,cur置空,前驱指针prev指向空
//尾删
void SLTPopBack(SLTNode **ps)
{
SLTNode *phead = *ps;
//只有一个结点
if (phead->next == NULL)
{
free(phead);
*ps = NULL;
phead = NULL;
}
//表为空直接返回
else if (*ps == NULL)
{
return ;
}
//剩多个结点
else
{
//定义前驱指针和目标指针
SLTNode *tail= *ps;
SLTNode *prev = NULL;
while (tail->next != NULL)
{
prev = tail;
tail= tail->next;
}
//移除最后一个结点,让前一个指针指向空
free(tail);
cur = NULL;
prev->next = NULL;
}
}
查找
查找54
找到该结点就返回,没找到就返回空,空表直接返回
//查找
SLTNode* SLTFind(SLTNode *ps, int x)
{
if (ps == NULL)
return NULL;
SLTNode *cur = ps;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
指定位置后插入
在pos后面插入一个新的结点,需要备份pos的下一个结点,先让newNode指向第二个结点再让pos指向newNode
//指定位置后插入
void SLTInsertAfter(SLTNode *pos, int x)
{
assert(pos);
SLTNode *next = pos->next;
SLTNode *newNode = BuySLTNode(x);
pos->next = newNode;
newNode->next = next;
}
指定位置前插入
1、在指定pos的前面插入,如果pos和ps在同一个位置就是头插了
2、如果pos出现在链表的中间某个位置,要想在pos前插入一个新的结点就必须要找到pos前的一个结点prev,让这个结点prev指向新的结点newNode,最后让newNode指向当前结点
//指定位置前插入
void SLTInsertBefor(SLTNode **ps, SLTNode *pos, int x)
{
assert(pos);
SLTNode *newNode = BuySLTNode(x);
if (pos == *ps)
{
newNode->next = *ps;
*ps = newNode;
}
else
{
SLTNode *cur = *ps;
SLTNode *prev = NULL;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = newNode;
newNode->next = cur;
}
}
指定位置后删除
next用来保存pos位置的下一个结点,pos指向next的下一个结点,释放next位置的结点
//移除指定位置之后
void SLTEraseAfter(SLTNode *pos)
{
//表为空不做处理
if(pos == NULL)
{
return ;
}
assert(pos);
SLTNode *next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
移除指定位置
prev和cur一起一后,当cur与pos相遇了,就将prev链接cur的下一个结点,释放pos位置的结点,将指针置空
如果plist和pos指向一起,就是头删操作了
//移除指定位置
void SLTEraseBefor(SLTNode ** ps, SLTNode *pos)
{
assert(pos);
SLTNode *next = pos->next;
//pos等于ps,头删
if (*ps == pos)
{
free(*ps);
*ps = NULL;
*ps = next;
}
else
{
SLTNode *prev = NULL;
SLTNode *cur = *ps;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = cur->next;
free(cur);
cur = NULL;
}
}
力扣题
移除链表元素:
点我.
考虑两种场景
- 1、如果表的第一个结点的值就是val,那么就是头删了,释放第一个结点,需要将head的指向改变,指向下一个结点
- 2、如果表的所有结点全是val的情况,head的指向就可以一直改变,直到指向NULL,最后返回去的就是NULL
链表移除结点的变形题,把特殊场景处理就能搞定
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode *prev = NULL;
struct ListNode *cur = head;
while(cur != NULL)
{
//值为val
struct ListNode *del = NULL;
if(cur->val != val)
{
prev = cur;
cur = cur->next;
}
//值不为val
else
{
//如果cur是头的情况
struct ListNode *next = cur->next;
if(prev == NULL)
{
free(cur);
head = next;
cur = next;
}
//cur不是头
else
{
prev->next = next;
free(cur);
cur = next;
}
}
}
return head;
}
第二种写法:带哨兵位的头节点,可以让prev指向哨兵位结点,这样就不需要考虑prev是否会存在空指针的问题了,同样的prev和cur一起一后,找到了值位val的结点,就释放该节点,cur继续指向下一个位置,最后只需要返回哨兵位结点的下一个位置
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
guard->next = head;
struct ListNode *prev = guard;
struct ListNode *cur = head;
while(cur != NULL)
{
if(cur->val != val)
{
prev = cur;
cur = cur->next;
}
else
{
struct ListNode *next = cur->next;
prev->next = next;
free(cur);
cur = next;
}
}
return guard->next;
}
206. 反转链表:
点我.
实现思路
使用三个指针迭代的方法,prev和cur用来逆置结点的指向,next保证cur能找到下一个结点的位置,直到cur指向空了,循环停止,返回prev指针,整个链表也就逆置了
struct ListNode* reverseList(struct ListNode* head){
if(head == NULL)
{
return NULL;
}
struct ListNode* cur = head;
struct ListNode* prev = NULL;
struct ListNode* next = cur->next;
while(cur)
{
//逆置
cur->next = prev;
//迭代
prev = cur;
cur = next;
if(next != NULL)
next = next->next;
}
return prev;
}
思路二:必做,链表头插的变型题,将原链表遍历一遍,取原链表的结点头插到新的表中
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;
}
876. 链表的中间结点:
点我.
快慢指针解法,快指针走两步慢指针走一步,当快指针走到链表NULL位置的时候,或者快指针走到最后一个结点的位置,循环终止,返回慢指针
struct ListNode* middleNode(struct ListNode* head){
if(head == NULL)
{
return NULL;
}
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
链表中倒数第k个结点:
点我.
快指针先走k步,快指针走完k步后,快指针和慢指针一起走,当快指针走完了,slow就是倒数第k个结点,就返回slow的位置
极端情况:如果fast先走k步,k大于链表的长度,链表可能存在空表,是空表就返回NULL
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
// write code here
struct ListNode* fast = pListHead;
struct ListNode* slow = pListHead;
while(k--)
{
//k大于链表长度,fast已经走到NULL了,
if(fast == NULL)
return NULL;
fast = fast->next;
}
while(fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
21. 合并两个有序链表
链接: 点我.
思路:合并两个链表只需要将原链表中最小的值尾插到新链表中去
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
if(l1 == NULL)
return l2;
if(l2 == NULL)
return l1;
if(l1 == NULL && l2 == NULL)
return NULL;
struct ListNode* newList = NULL;
struct ListNode *tail = newList;
//取下最小值做新链表的头
if(l1->val < l2->val)
{
newList = tail = l1;
l1 = l1->next;
}
else
{
newList = tail = l2;
l2 = l2->next;
}
//取两个链表中最小值尾插到新的链表中去
while(l1 && l2)
{
if(l1->val < l2->val)
{
tail->next = l1;
l1 = l1->next;
}
else
{
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
//如果有一个链表还没遍历完就继续尾插到新链表去
if(l1)
{
tail->next = l1;
}
if(l2)
{
tail->next = l2;
}
return newList;
}
带哨兵位的头节点不需要考虑新链表头尾都是是NULL的这种情况
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
//创建哨兵头节点
struct ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* tail = head;
tail->next = NULL;
while(l1 && l2)
{
if(l1->val < l2->val)
{
tail->next = l1;
l1 = l1->next;
}
else
{
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
//还剩的
if(l1){
tail->next = l1;
}
if(l2){
tail->next = l2;
}
//释放动态开辟的结点,返回新的头
struct ListNode* node = head;
head = head->next;
free(node);
return head;
}
OR36 链表的回文结构
链接: 点我 .
解题思路:先找出中间结点(快慢指针法),将中间结点起始位置往后整体逆置,返回逆置后的头指针,得到后部分逆置的链表,再跟原链比较
class PalindromeList {
public:
struct ListNode* middleNode(struct ListNode* head){
if(head == NULL)
{
return NULL;
}
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
struct ListNode* reverseList(struct ListNode* head){
if(head == NULL)
{
return NULL;
}
struct ListNode* cur = head;
struct ListNode* prev = NULL;
struct ListNode* next = cur->next;
while(cur)
{
//逆置
cur->next = prev;
//迭代
prev = cur;
cur = next;
if(next != NULL)
next = next->next;
}
return prev;
}
bool chkPalindrome(ListNode* A) {
// write code here
//找到中间结点整体逆置,最后比较
struct ListNode* mid = middleNode(A);
struct ListNode* rhead = reverseList(mid);
//比较
while(A && rhead)
{
if(A->val != rhead->val)
{
return false;
}
else
{
A = A->next;
rhead = rhead->next;
}
}
return true;
}
};
160. 相交链表
链接: 点我.
链表的相交并不是直线的相交,而是两个链表中存在一个公共结点,这样的链表我们可以称它是相交链表,比较结点的地址,如果相同那么就是相交的,否则不是,让长距离的先走差距步,最后再同时走,如果中间相交就返回该结点,否则返回NULL
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
//记录链表的长度
int lenA = 0,lenB = 0;
struct ListNode *curA = headA;
struct ListNode *curB = headB;
while(curA->next)
{
lenA++;
curA = curA->next;
}
while(curB->next)
{
lenB++;
curB = curB->next;
}
//如果不相遇那么链表的最后一个结点一定不同
if(curA != curB)
return NULL;
//定义长的链表,定义短的链表
struct ListNode * longlist = headA, *shortlist = headB;
int gap = abs(lenB - lenA);
if(lenA < lenB)
{
longlist = headB;
shortlist = headA;
}
//长链表先走gap步
while(gap--)
{
longlist = longlist->next;
}
//最后一起走
while(longlist != shortlist)
{
longlist = longlist->next;
shortlist = shortlist->next;
}
return longlist;
}
141. 环形链表
链接: 点我.
快慢指针解法,快指针走两步,慢指针走一步,当他们相遇了,就能证明链表是环形链表,如果fast走到NULL的位置或者fast的下一个结点是空了,那么链表就不为环形
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;
}
延申问题
-
1、为什么slow走一步,fast走两步,它们一定会在环里面相遇,会不会永远追不上,请证明
-
2、slow走一步,fast走3步?走4步?走n步行不行?请证明!
-
3、求环的入口点
不会,假设slow进环的时候fast跟slow的差距是N, 紧接着追击的过程,fast往前走2步,slow走1步,它们每次一走,它们之间的距离缩小1,N-1 N-2 … 2 1 0,直到等于0的时候就相遇了
2、slow走一步,fast走3步?走4步?走n步行不行?请证明!
假设slow进环的时候fast跟slow的差距是N, 紧接着追击的过程,fast往前走3步,slow走1步,它们每次一走,它们之间的距离缩小2,会分两种情况,如果是奇数个,如果是偶数个呢?如果是偶数个可以相遇,如果是奇数(-1)个并不会相遇,假设环的大小是C,那么slow和falst的差距就是C-1了,如果C-1恰好是奇数就永远追不上
总结:如果slow进环时,slow跟fast的差距是奇数,且环的长度是偶数,那么它们两个再环里面就会一直打圈
解题思路:
将fast和slow的相遇点找出来,meet表示相遇点,从相遇点meet开始
head走一步meet走一步,如果他们相遇了,那么这就是入口点
struct ListNode *detectCycle(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)
{
//相遇后,meet从相遇点开始走,head从起点开始走,
struct ListNode *meet = fast;
while(head != meet)
{
head = head->next;
meet = meet->next;
}
//当head和meet相遇了,这就是入口点
if(meet == head)
{
return meet;
}
}
}
return NULL;
}
138. 复制带随机指针的链表:
点我.
解题思路:
要想复制一份链表比较得保证新链表的结点关系和原链表保持一致,并且新链表的每个结点的值以及结点中两个指针的指向都必须与原链表结点一样,将拷贝链表的结构构建出来后,同步拷贝链表和原链表两个指针的指向,最后将拷贝链表拆解下来,同时还原原链表结点的链接的关系,最后返回拷贝链表的头
struct Node* copyRandomList(struct Node* head) {
if(head == NULL)
{
return NULL;
}
struct Node* cur = head;
//建立拷贝结点的关系
while(cur)
{
struct Node* next = cur->next;
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
copy->next = next;
cur->next = copy;
cur = next;
}
//同步拷贝结点的随机指针
cur = head;
while(cur)
{
struct Node* copy = cur->next;
struct Node* next = copy->next;
if(cur->random == NULL)
{
copy->random = NULL;
}
else
{
copy->random = cur->random->next;
}
cur = next;
}
//创建哨兵位头节点,新链表的尾结点
cur = head;
struct Node* copyhead,*copytail;
copyhead = copytail = (struct Node*)malloc(sizeof(struct Node));
while(cur)
{
//复原新链表的关系
struct Node* copy = cur->next;
struct Node* next = copy->next;
copytail->next = copy;
copytail = copytail->next;
cur->next = next;
cur = next;
}
//释放头结点
struct Node* guard = copyhead;
copyhead = copyhead->next;
free(guard);
return copyhead;
}
147. 对链表进行插入排序
链接: 点我.
解题思路:
取下原链表中的第一个结点将他当成一个有序的新链表看待,再比较剩余结点构成的链表,比较完后将旧链表中剩余的结点按照顺序方式插入到新的链表中
struct ListNode* insertionSortList(struct ListNode* head){
if(head == NULL || head->next == NULL)
{
return head;
}
struct ListNode* cur = head->next;
struct ListNode* rhead = head;
rhead->next = NULL;
while(cur)
{
struct ListNode* p = NULL;
struct ListNode* c = rhead;
struct ListNode* next = cur->next;
while(c)
{
if(cur->val < c->val)
{
break;
}
else
{
p = c;
c = c->next;
}
}
//头插
if(p == NULL)
{
cur->next = c;
rhead = cur;
}
//顺序位置处插入
else
{
p->next = cur;
cur->next = c;
}
cur = next;
}
return rhead;
}