【数据结构】链表总结

本文介绍了链表作为一种非连续存储结构,包括单链表和双向循环链表的概念、结构、分类以及各种操作的实现,如插入、删除、查找等。同时,讨论了不同链表结构在头插头删操作的时间复杂度优势和任意位置插入删除的缺点,并通过实例展示了如何解决这些问题。此外,文章还涵盖了链表在实际应用中的优缺点及其在编程面试中的常见问题,如链表的回文结构、相交节点等。
摘要由CSDN通过智能技术生成

一、链表

    • 链表的引入:

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

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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值