一、链表
- 链表的引入:
在使用顺序表的过程中,我们能感受到顺序表有以下几个缺点
1)空间不够,需要扩容。扩容(异地)是有一定代价的,其次还可能存在一定的空间浪费。
2)头部或者中部的插入删除,需要挪动数据,效率低下。
面对以上几个问题,我们引入链表结构来解决。
- 链表的概念及结构:
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
链表的逻辑结构:
链表的现实结构:
注意:
从图中我们可以看出,链表在逻辑结构中是连续的,但在物理结构上链表不一定连续。
链表的结点一般是从堆中申请,而申请的空间不一定连续。
- 链表的分类:
链表的结构非常繁多,但实际应用中我们只能用到两类链表:
单向不带头不循环链表:
双向带头循环链表:
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
- 链表的实现:
单向不带头不循环链表:
4.1单链表结构创建及功能
typedef int SLDataType;
//单链表
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLTNode;
//创建节点
SLTNode* BuySLTNode(SLDataType x);
//向链表中填入数据
SLTNode* CreateSList(int n);
//打印链表
void SLTPrint(SLTNode* phead);
//尾插尾删
void SLTPushBack(SLTNode** pphead, SLDataType x);
void SLTPopBack(SLTNode** pphead);
//头插头删
void SLTPushFront(SLTNode** pphead,SLDataType x);
void SLTPopFront(SLTNode* pphead);
//查找节点
SLTNode* SLTFind(SLTNode* phead, SLDataType x);
//单链表在pos位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLDataType x);
//单链表删除pos后一位的数据
void SLTEraseAfter(SLTNode* pos);
//单链表在pos之前插入数据
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLDataType x);
//单链表删除pos位置的数据
void SLTErase(SLTNode** pphead,SLTNode* pos);
//释放单链表空间
void SLTDestroy(SLTNode** pphead);
在实现功能前,我们需要弄懂为什么有的函数传的参数不一样?
仔细观察,在一些函数中传二级指针,都有一个改头结点的操作,那为什么改头结点需要使用二级指针?
引例:
void Swap1(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void Swap2(int** pp1, int** pp2)
{
int* tmp = *pp1;
*pp1 = *pp2;
*pp2 = tmp;
}
void Test()
{
int a = 0, b = 1;
int* ptr1 = &a, * ptr2 = &b;
//交换a与b
//即a原本为0,b为1,交换后a为1,b为0
Swap1(&a, &b);
//交换ptr1与ptr2
//即ptr1原本指向a,ptr2指向b,交换后ptr1指向b,ptr2指向a
Swap2(&ptr1, &ptr2);
}
众所周知,在函数中,对于形参的修改不会影响到实参,会导致函数不会起到预期的作用,因此对于修改实参,我们采用指针的方法。
同理,头结点的类型是SLTNode*,如果我们使用SLTNode*类型的指针修改头结点的指向,形参的改变不会影响实参,头结点的指向不会发生改变,而对于SLTNode*类型的修改,我们采用二级指针来修改它的指向,因此传的参数为SLTNode**类型。
4.2单链表的功能实现:
//创建节点
SLTNode* BuySLTNode(SLDataType x)
{
SLTNode* newnode = malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//创建链表(将节点链接成链表)
SLTNode* CreateSList(int n)
{
//phead用于找到头节点 ptail用于链接节点
SLTNode* phead = NULL, *ptail = NULL;
int x = 0;
for (int i = 0; i < n; i++)
{
//scanf("%d", &x);
//SLTNode* newnode = BuySLTNode(x);//创建节点
SLTNode* newnode = BuySLTNode(i);
if (phead == NULL)
{
ptail = phead = newnode;
}
else
{
ptail->next = newnode;//链接新的节点
ptail = newnode;//让ptail成为下一个节点
}
}
return phead;
}
//打印链表
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
//打印整个节点
/*printf("[%d|%p]->", cur->data, cur->next);*/
cur = cur->next;//将下一个节点的地址赋值给cur
}
printf("NULL\n");
}
//尾插
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
//将Plist指向newnode,即将空表的表头链接节点
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
//找尾
while (tail->next)
{
tail = tail->next;
}
//链接
tail->next = newnode;
}
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(*pphead);//表不能为空
if ((*pphead)->next == NULL)//表中只剩一个节点
{
free(*pphead);
*pphead = NULL;
}
else
{
//方法一:
//SLTNode* tail = *pphead;
//SLTNode* prev = NULL;//找到尾删的前一项
找尾
//while (tail->next)
//{
// prev = tail;
// tail = tail->next;
//}
//free(tail);
//prev->next = NULL;
//方法二:
SLTNode* tail = *pphead;
//找尾
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;//将节点与单链表链接
*pphead = newnode;//让新节点成为头节点
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(*pphead);
SLTNode* next = (*pphead)->next;//找到表中第二个节点
free(*pphead);//释放删除节点的空间
*pphead = next;//让第二个节点成为头节点
}
//查找节点
SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if ((cur->data) == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//单链表在pos位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;//先将pos的后一位链接到新节点
pos->next = newnode;//将新节点链接到pos上
}
//单链表删除pos后一位的数据
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* prev = pos->next;//保存pos后一位的地址
pos->next = prev->next;//将pos与prev的下一个节点链接
free(prev);//释放pos后一位节点空间
}
}
//单链表在pos之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
assert(pos);
if (*pphead == pos)//当pos为表头就是头插
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;//保存pos前一节点的地址
while (prev->next != pos)//寻找pos前一节点
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos;//链接节点-后
prev->next = newnode;//链接节点-前
}
}
//单链表删除pos位置的数据
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pos);
assert(*pphead);
if (*pphead == pos)//当pos为表头就是头删
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;//保存pos前一节点
while (prev->next != pos)//寻找pos前一节点
{
prev = prev->next;
}
prev->next = pos->next;//删除pos节点,链接前后节点
free(pos);//释放pos节点空间
}
}
//释放单链表空间
void SLTDestroy(SLTNode** pphead)
{
SLTNode* cur = *pphead; //替身指针 (释放节点)
while (cur)
{
SLTNode* next = cur->next; //保存释放空间的下一个节点地址
free(cur);//释放链表节点
cur = next;//释放下一个节点
}
*pphead = NULL;//将链表表头指针置为NULL,防止再次错误调用。(野指针)
}
相比于顺序表,单链表有优点,相对的这种数据结构也有缺点
优点:头插头删,时间复杂度为O(1)。
缺点:当我们想要在任意位置插入删除数据,需要记录他的前一个节点,这就需要去寻找这个节点,需要遍历一遍整个链表,因此时间复杂度为O(N)。
既然单链表这种数据结构有这种缺陷,那是否有4方法来解决这个缺点?
对此我们引入带头双向循环链表这种数据结构来解决整个问题。
4.3循环链表结构创建及功能:
typedef int DLDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
DLDataType data;
}DLTNode;
//创建节点
DLTNode* BuyNode(DLDataType x);
//初始化头节点
DLTNode* DListInit();
//打印聊表:
void DListPrint(DLTNode* pHead);
//尾插尾删
void DListPushBack(DLTNode* pHead,DLDataType X);
void DListPopBack(DLTNode* pHead);
//头插头删
void DListPushFront(DLTNode* pHead,DLDataType x);
void DListPopFront(DLTNode* pHead);
//查找
DLTNode* DLFind(DLTNode* pHead,DLDataType x);
//任意位置插入删除
void DLInsert(DLTNode* pos, DLDataType* x);//pos前
void DLErase(DLTNode* pos);
//判断链表为空
bool DLEmpty(DLTNode* pHead);
//链表数据个数
size_t DLSize(DLTNode* pHead);
//销毁链表
void DLDestroy(DLTNode* pHead);
为什么在这个链表中,函数的参数没有使用二级指针?
在实现这个链表时,我们使用哨兵位的头结点,因此在链接或删除结点时,仅仅改变结点中的next或者prev指针,实际上就是改变结构体,使用结构体指针就可以,不需要使用二级指针。
4.4循环链表的功能实现:
//创建节点
DLTNode* BuyNode(DLDataType x)
{
DLTNode* newnode = (DLTNode*)malloc(sizeof(DLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->next = NULL;
newnode->prev = NULL;
newnode->data = x;
return newnode;
}
//初始化头节点
DLTNode* DListInit()
{
DLTNode* pHead = BuyNode(-1);
pHead->next = pHead;
pHead->prev = pHead;
return pHead;
}
//打印链表
void DListPrint(DLTNode* pHead)
{
assert(pHead);
DLTNode* cur = pHead->next;
while (cur != pHead)
{
printf("-%d", cur->data);
cur = cur->next;
}
printf("\n");
}
//尾插尾删
void DListPushBack(DLTNode* pHead, DLDataType x)
{
assert(pHead);
//DLTNode* newnode = BuyNode(x);
//DLTNode* tail = pHead->prev;//找到尾节点
//tail->next = newnode;//与前一节点双向链接
//newnode->prev = tail;
//newnode->next = pHead;//与头节点双向链接
//pHead->prev = newnode;
DLInsert(pHead, x);
}
void DListPopBack(DLTNode* pHead)
{
assert(pHead);
assert(pHead->next != pHead);
//DLTNode* tail = pHead->prev;//找到尾节点
//DLTNode* tailprev = tail->prev;//找到尾节点的前节点
//pHead->prev = tailprev;//将前节点与头节点双向链接
//tailprev->next = pHead;
//free(tail);
DLErase(pHead->prev);
}
//头插头删
void DListPushFront(DLTNode* pHead, DLDataType x)
{
assert(pHead);
//DLTNode* newnode = BuyNode(x);
//DLTNode* tail = pHead->next;//找到头节点下一位
//newnode->next = tail;//将插入节点与tail相连
//tail->prev = newnode;
//newnode->prev = pHead;//将插入节点与头节点相连
//pHead->next = newnode;
DLInsert(pHead->next, x);
}
void DListPopFront(DLTNode* pHead)
{
assert(pHead);
assert(pHead->next != pHead);
//DLTNode* cur = pHead->next;//找到删除节点
//DLTNode* next = cur->next;//找到删除节点的下一位
//pHead->next = next;//链接头节点与下一位节点
//next->prev = pHead;
//free(cur);
DLErase(pHead->next);
}
//查找
DLTNode* DLFind(DLTNode* pHead,DLDataType x)
{
assert(pHead);
DLTNode* cur = pHead->next;
while(cur != pHead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//任意位置插入删除
void DLInsert(DLTNode* pos, DLDataType* x)
{
assert(pos);
DLTNode* newnode = BuyNode(x);
DLTNode* posprev = pos->prev;//pos的前一节点
//链接三个节点 posprev newnode pos
posprev->next = newnode;//新节点与前节点链接
newnode->prev = posprev;
newnode->next = pos;//新节点与pos位置链接
pos->prev = newnode;
}
void DLErase(DLTNode* pos)
{
assert(pos);
DLTNode* posprev = pos->prev;//找到前一个节点
DLTNode* posnext = pos->next;//找到后一个节点
//链接 posprev posnext
posprev->next = posnext;
posnext->prev = posprev;
free(pos);
}
//判断链表为空
bool DLEmpty(DLTNode* pHead)
{
assert(pHead);
/*if (pHead->next == pHead)
{
return true;
}
else
return false;*/
return pHead->next == pHead;
}
//链表数据个数
size_t DLSize(DLTNode* pHead)
{
assert(pHead);
size_t sum = 0;
DLTNode* cur = pHead->next;
while (cur != pHead)
{
sum++;
cur = cur->next;
}
return sum;
}
//销毁链表
void DLDestroy(DLTNode* pHead)
{
assert(pHead);
DLTNode* cur = pHead->next;
while (cur != pHead)
{
DLTNode* next = cur->next;
free(cur);
cur = next;
}
free(pHead);
}
二、链表的OJ
- 删除链表中等于给定值 val 的所有节点
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
思路:创建一个新的头结点,遍历源链表,将不等于val的节点链接到创建的新节点后
代码实现:
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* rhead, *tail;
tail = rhead = (struct ListNode*)malloc(sizeof(struct ListNode));//哨兵位
tail->next = NULL;
struct ListNode* cur = head;
while(cur)
{
struct ListNode* next = cur->next;//记录下一节点
if(cur->val != val)
{
tail->next = cur;//链接到新节点
cur->next = NULL;//将已链接的节点与原链表断开
tail = tail->next;
}
cur = next;
}
return rhead->next;
}
- 反转一个链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
思路1:遍历原链表,将数据头插到新链表
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* cur = head;
struct ListNode* rhead = NULL;
while(cur)
{
struct ListNode* next = cur->next;//保留下一个节点
//头插
cur->next = rhead;
rhead = cur;
cur = next;
}
return rhead;
}
思路2:三指针迭代,将链表反转
struct ListNode* reverseList(struct ListNode* head) {
if (head == NULL || head->next == NULL)//链表为空,或者就一个元素
return head;
struct ListNode* prev = NULL, * cur = head, * next = head->next;
while (cur)
{
//反转链表
cur->next = prev;
//迭代
prev = cur;
cur = next;
if (next)
next = next->next;
}
return prev;
}
3.给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
思路:设置快慢指针,快指针一次走两部,慢指针走一步,奇数个和偶数个时情况不同。
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast, *slow;
fast = slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
4.输入一个链表,输出该链表中倒数第k个结点
输入一个链表,输出该链表中倒数第k个结点。
思路:设置快慢指针,通过观察,让快指针先走k步,然后快指针和慢指针同时走,当快指针为空时,慢指针所在位置即为倒数第k位。
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
struct ListNode* fast,*slow;
fast = slow = pListHead;
while(k--)
{
//链表没有k步
if(fast == NULL)
{
return NULL;
}
fast = fast->next;
}
while(fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
5.将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:新建一个头结点,遍历两条链表,并比较其val大小,小的链接到新链表中。
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode* guard,*tail;
guard = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
guard ->next = NULL;
while(list1 && list2)
{
if(list1->val < list2->val)
{
tail->next = list1;
tail = tail->next;
list1 = list1->next;
}
else
{
tail->next = list2;
tail = tail->next;
list2 = list2->next;
}
}
if(list1)//第一条剩余
{
tail->next = list1;
}
if(list2)//第二条剩余
{
tail->next = list2;
}
struct Node* head = guard->next;//返回哨兵的下一位才是链表
free(guard);
return head;
}
6.编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。
现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
思路:新创建两个链表,一个用来链接小于x的,另一个用来链接大于x的,然后将两条链表链接,将合并的链表链接到原链表头节点。
class Partition {
public:
ListNode* partition(ListNode* pHead, int x) {
struct ListNode* cur = pHead;
struct ListNode* lessHead,*lessTail,*greaterHead,*greaterTail;
lessTail = lessHead = (ListNode*)malloc(sizeof(ListNode));
greaterTail = greaterHead = (ListNode*)malloc(sizeof(ListNode));
lessTail->next = NULL;
greaterTail->next = NULL;
while(cur)
{
if(cur->val<x)
{
lessTail->next = cur;
lessTail = lessTail->next;
}
else
{
greaterTail->next = cur;
greaterTail = greaterTail->next;
}
cur = cur->next;
}
lessTail->next = greaterHead->next;
greaterTail->next = NULL;
pHead = lessHead->next;
free(lessHead);
free(greaterHead);
return pHead;
}
};
7.链表的回文结构
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
思路:先找到链表的中间位置的节点,然后将中间节点以后的节点逆置,创建一个新节点,然后将逆置后的链表链接到新节点后,比较原链表与新链表,若前半段相同,则为回文。
class PalindromeList {
public:
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* fast, *slow;
fast = slow = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}//找中间功能
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* cur = head;
struct ListNode* rhead = NULL;
while (cur) {
struct ListNode* next = cur->next;//保留下一个节点
//头插
cur->next = rhead;
rhead = cur;
cur = next;
}
return rhead;
}//逆置功能
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;
}
A = A->next;
rhead = rhead->next;
}
return true;
}
};
8.输入两个链表,找出它们的第一个公共结点。
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
思路:首先判断两条链表是否相交,即判断链表的最后一个节点地址是否相同,求出两条链表的长度,设置两个指针用来遍历链表,求出长度的差值,让长的链表的指针先走差值的步数,然后两个指针同时走,俩指针相等,即为交点。
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* curA = headA;
struct ListNode* curB = headB;
int lenA = 0, lenB = 0;
//求得A、B链的长度
while(curA->next)
{
lenA++;
curA = curA ->next;
}
while(curB->next)
{
lenB++;
curB = curB->next;
}
//判断是否相交 (链尾相同)
if(curA != curB)
{
return NULL;
}
//求出两链表个数差值
int gap = abs(lenA-lenB);
//区分长短链
struct ListNode* longlist = headA, *shortlist = headB;
if(lenA < lenB)
{
shortlist = headA;
longlist = headB;
}
//长链先遍历差距步
while(gap--)
{
longlist = longlist->next;
}
//同时遍历
while(longlist != shortlist)
{
longlist = longlist->next;
shortlist = shortlist->next;
}
return longlist;
}
9.给定一个链表,判断链表中是否有环
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
思路:设置快慢,快指针一次走两步,慢指针一次走一步,当慢指针入环时,问题转化为快指针追赶慢指针,根据相对运动,两个指针每走一次,相对距离就会减小一,快指针就会离慢指针更近一步,最终二者在环中相遇,即为环,如果不是环,快指针一定先走到末端。
bool hasCycle(struct ListNode *head)
{
struct ListNode* fast = head, *slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
return true;
}
}
return false;
}
扩展问题:
为什么快指针每次走两步,慢指针走一步可以?
假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在满指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。
快指针一次走3步,走4步,...n步行吗?
不一定,假设快指针一次走3步,慢指针在入环后,快指针以相对2个单位追赶慢指针,如果N为奇数,N为俩指针之间的距离,则他俩之间的距离变化为:N-2,N-4,.........3,1,-1.当变为-1时,则又开始新一轮的追击,而他俩之间的距离为C-1,C为整个环的周长,如果C-1为偶数,则可以追赶上,若为奇数,则追不上。
由以上可知,只要fast与slow的相对速度为1,无论如何都可以追上。
10.给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
思路:
我们观察可以看出,这一类问题可总结出规律:
假设进环前长度为L,环的周长为C,进环点与相遇点的距离为N
公式推导:
fast走的距离 = 2*slow走的距离
slow走的距离 = L+N
fast走的距离 = L+n*C+N(slow进环前,fast已经走了n圈)
为什么slow走的不是L+n*C+X呢? 即为什么slow在圈里一定走了不到一圈就相遇了呢?我们知道当slow刚刚进圈时slow与fast之间的距离一定小于C-1,fast一次走两步,slow一次走一步,距离逐渐减小,即一定走了小于C-1次就会相遇,因此推出此时slow走了不到一圈。
则 2*(L+N) = L+n*C+N,即L = n*C-N
得到:
由此可以,一个指针在头结点,一个指针在相遇点,两指针同时走,一定可以相遇,相遇点即为进环点。
同时问题也可以转化成相交问题,我们可以创建一个指针记录相遇点meet的后一位节点,然后令其与相遇点断开,那么问题就变成了链链表的相交问题。
公式法:
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 = slow;
while(head != meet)
{
head = head->next;
meet = meet->next;
}
return meet;
}
}
return NULL;
}
相交法:
struct ListNode* hasCycle(struct ListNode *head)
{
struct ListNode* fast = head, *slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
return slow;
}
}
return NULL;
}
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* curA = headA;
struct ListNode* curB = headB;
int lenA = 0, lenB = 0;
//求得A、B链的长度
while(curA->next)
{
lenA++;
curA = curA ->next;
}
while(curB->next)
{
lenB++;
curB = curB->next;
}
//判断是否相交 (链尾相同)
if(curA != curB)
{
return NULL;
}
//求出两链表个数差值
int gap = abs(lenA-lenB);
//区分长短链
struct ListNode* longlist = headA, *shortlist = headB;
if(lenA < lenB)
{
shortlist = headA;
longlist = headB;
}
//长链先遍历差距步
while(gap--)
{
longlist = longlist->next;
}
//同时遍历
while(longlist != shortlist)
{
longlist = longlist->next;
shortlist = shortlist->next;
}
return longlist;
}
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* meet = hasCycle(head);
if(meet== NULL)
{
return NULL;
}
struct ListNode* rhead = meet->next;//记录相遇点的下一节点
meet->next = NULL;//断开环,防止进入死循环
struct ListNode* cur = head;
struct ListNode* meetagain = getIntersectionNode(cur,rhead);
meet->next = rhead;//恢复环
return meetagain;
}
11.给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中任何节点或空节点。要求返回这个链表的深度拷贝。
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
思路:
方法一:
通过暴力的方法来查找random指针,通过i来记录random指针指向的节点位于整个链表的第几位,这种方法首先需要挨个遍历每个节点,找到节点random指针的指向,并用i来记录该节点是链表的第几个,从而当处理拷贝链表的过程中,再利用双层循环将每个特定的位置的random指向这个拷贝链表相对应的位置。
方法二:
1.将链表的每一个节点拷贝出来,并链接到原节点的后一为
2.设置拷贝节点的random指针,拷贝节点的random指针指向的就是原节点的random指针指向的节点的后一节点(拷贝节点就在原节点后)。
3.将拷贝节点与原节点断开,并链接,恢复原链表。
struct Node* copyRandomList(struct Node* head)
{
//将链表每个节点拷贝并链接到本体后
struct Node* cur = head;
while(cur)
{
struct Node* next = cur->next;
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
//链接拷贝节点
cur->next = copy;
copy->next = next;
cur = next;
}
//设置拷贝节点的random指针
cur = head;
while(cur)
{
struct Node* copy = cur->next;
if(cur->random == NULL)
{
copy->random = NULL;
}
else
{
copy->random = cur->random->next;
}
cur = copy->next;
}
//将拷贝节点链接
cur = head;
struct Node* copyHead = NULL, *copyTail=NULL;
while(cur)
{
struct Node* copy = cur->next;
struct Node* next = copy->next;
cur->next = next;
if(copyTail == NULL)
{
copyTail = copyHead = copy;
}
else
{
copyTail->next =copy;
copyTail = copyTail->next;
}
cur = next;
}
return copyHead;
}