【数据结构】链表总结

一、链表

    • 链表的引入:

在使用顺序表的过程中,我们能感受到顺序表有以下几个缺点

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 的所有节点

203. 移除链表元素

给你一个链表的头节点 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;
}
    • 反转一个链表

206. 反转链表

给你单链表的头节点 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 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点

876. 链表的中间结点

给定一个头结点为 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步,然后快指针和慢指针同时走,当快指针为空时,慢指针所在位置即为倒数第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.链表的回文结构

OR36 链表的回文结构

对于一个链表,请设计一个时间复杂度为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.输入两个链表,找出它们的第一个公共结点。

160. 相交链表

给你两个单链表的头节点 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.给定一个链表,判断链表中是否有环

141. 环形链表

给你一个链表的头节点 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

142. 环形链表 II

给定一个链表的头节点 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.给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中任何节点或空节点。要求返回这个链表的深度拷贝。

138. 复制带随机指针的链表

给你一个长度为 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;
}

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值