数据结构-单链表

1. 单链表介绍

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

相比于顺序存储结构(一般指数组),链表在插入和删除的时候可以达到O(1)的时间复杂度,如果你的数据容器需频繁进行插入删除操作,那链表是不错的选择。单向链表结构图如下所示:

单向链表结构

2. 单链表常见问题汇总

链表是最基本的数据结构,在面试中,面试官也常常会选择链表来考察面试者的基本能力。主要由于链表的实现涉离不开指针,而指针往往又很容易出错,因此使得链表成为面试中一个比较好的考察点。

那这边是我从互联网上收集到的,在面试及生产中用的比较多的单链表知识点,具体如下:

(主要参考于 http://blog.csdn.net/luckyxiaoqiang/article/details/7393134#topic2)

1. 设计链表,实现链表基本的创建,添加,删除,查询操作

2. 合并两个有序链表

3. 反转链表

4. 删除链表的倒数第K个节点(k>0)

5. 查找链表的中间结点

6. 判断一个单链表中是否有环

7. 求两个单链表相交的第一个节点

8. 已知一个单链表中存在环,求进入环中的第一个节点

9. 给出一单链表头指针pHead和一节点指针pToBeDeleted,O(1)时间复杂度删除节点pToBeDeleted

3. 单链表基本实现(C++)

首先,我们实现一下对单链表的基本封装,具体代码如下: (编译平台:Linux centos 7.0 编译器:gcc 4.8.5 )

由于采用C++ 类进行封装,头节点不会提供给外部使用,因此对于处理多条链表关系,以及有环等问题,会专门用一节来整理,这些问题更偏向于提供一个解决问题的思路,偏算法,至于单链表数据结构,下面的封装基本可以满足。

头文件: single_list.h

#include <iostream>

/** @file single_list.h
 * 
 *  This is an list header file, implement single list warppes  
 * 
 *  Created by yejy on 18-8-21
 *  copyright (c) yejy. all rights reserved
 * 
 */

#define INVALID_VALUE  -1

struct _single_list_node
{
    int dataEntry;
    _single_list_node* Next;
};

/* single list */
class T_Single_List
{
  typedef _single_list_node* Link_type;

public:
  T_Single_List()
  { 
    pHeadNode = new _single_list_node();
    pHeadNode->Next = nullptr; 

    _size = 0;
  }

  ~T_Single_List()
  {
    clear();

    if(pHeadNode)
    {
      delete pHeadNode;
      pHeadNode = nullptr;
    }

    _size = 0;
  }

    // head | data -> insert2 | data -> insert1 | data -> null
  void InsertValueAtHead(int value) // 前插
  {
    if(pHeadNode)
    {
       Link_type pNode = new _single_list_node();

      if(pNode)
      {
        pNode->dataEntry = value;
        pNode->Next = pHeadNode->Next;
        pHeadNode->Next = pNode; 

        ++_size;
      }
    }
  }

  // head | data -> insert1 | data -> insert2 | data -> null
  void InsertValueAtTail(int value) // 尾插
  {
    if(pHeadNode)
    {
      Link_type pNode = new _single_list_node();
      Link_type pPollNode = pHeadNode; //

      if(pNode)
      {
        // 单向链表实现尾插,需要遍历到最后一个节点插入
        while(pPollNode->Next != nullptr)
        {
          pPollNode = pPollNode->Next;
        }

        pNode->Next = nullptr;
        pNode->dataEntry = value;
        pPollNode->Next = pNode; 

        ++_size;
      }
    }
  }

  // 按序号插入
  void InsertValueAtIndex(int index, int val)
  {
    if(index > _size || index <= 0)
    {
      std::cout << "error Not in range" << std::endl; 
      return ;
    }

    Link_type pPollNode = findbyindex(index);
    Link_type pNode = new _single_list_node();

    if(pNode)
    {
      pNode->Next = pPollNode->Next;
      pPollNode->Next = pNode;
      pNode->dataEntry = val;

      ++_size;
    }
  }

  // 根据index删除
  void deleteValueAtIndex(int index)
  {
    if(index > _size || index <= 0)
    {
      std::cout << "error Not in range" << std::endl; 
      return ;
    }

    Link_type pNode = findbyindex(index); //找到前一个
    RemoveNext(pNode); //删除Next即可
  }

  // 根据index获取值
  int GetValueByIndex(int index)
  {
    if(index > _size || index <= 0)
    {
      std::cout << "error Not in range" << std::endl; 
      return INVALID_VALUE;
    }

    Link_type pNode = findbyindex(index);

    if(pNode->Next)
    {
      return pNode->Next->dataEntry;
    }

    return INVALID_VALUE;
  }

  // 修改值
  void update(int value, int modify_value)
  {
    Link_type pNode = findbyvalue(value);

    if(pNode->Next)
    {
        pNode->Next->dataEntry = modify_value;
    }
  }

  // 查找链表的中间结点 使用快慢指针处理
  // 一个step为1 另一个step为2
  Link_type GetMidNode()
  {
    Link_type pNodeStep1 = pHeadNode;
    Link_type pNodeStep2 = pHeadNode;

    // while(pNodeStep2)
    // {
    //   pNodeStep2 = pNodeStep2->Next;
    //   if(pNodeStep2)
    //   {
    //     pNodeStep2 = pNodeStep2->Next;
    //   }
    //   else
    //   {
    //     break;
    //   }

    //   pNodeStep1 = pNodeStep1->Next;
    // }

    while(pNodeStep2 && pNodeStep2->Next)
    {
      pNodeStep1 = pNodeStep1->Next;
      pNodeStep2 = pNodeStep2->Next->Next;
    }

    return pNodeStep1;
  }

  // 求单链表反转 O(n)
  void ReverseList()
  {
    if(pHeadNode->Next == nullptr)
    {
      return;
    }

    Link_type pNode = pHeadNode->Next->Next;
    pHeadNode->Next->Next = nullptr;
    _size = 1;

    while(pNode)
    {
      Link_type pInsertNode = pNode;
      pNode = pNode->Next;

      pInsertNode->Next = pHeadNode->Next;
      pHeadNode->Next = pInsertNode; 
    }
  }

  // 查找链表中倒数第k个节点的值( n - k + 1 )
  int GetRePosNodeValue(int k)
  {
    if(k > _size || k <= 0)
    {
      std::cout << "error Not in range" << std::endl; 
      return INVALID_VALUE;
    }

    Link_type pNode = findbyindex(_size - k + 1);

    if(pNode->Next)
    {
      return pNode->Next->dataEntry;
    }

    return INVALID_VALUE;
  }

    // 清空链表
  void clear()
  {
    Link_type pNode = pHeadNode;

    while(pNode->Next)
    {
      RemoveNext(pNode);
    }
  }

  // 打印
  void printf()
  {
    if(_size <= 0)
    {
      return;
    }

    Link_type pNode = pHeadNode->Next;
    while(pNode)
    {
      std::cout << pNode->dataEntry << " ";

      pNode = pNode->Next;
    }

    std::cout << std::endl;
  }


private:
  // 查找 (返回前一个指针)
  Link_type findbyvalue(int value)
  {
    Link_type pNode = pHeadNode;
    while(pNode)
    {
      if(pNode->Next && pNode->Next->dataEntry == value)
      {
        return pNode;
      }

      pNode = pNode->Next;
    }

    return nullptr;
  }

  Link_type findbyindex(int index)
  {
    int i = 0;
    Link_type pNode = pHeadNode;

    while(pNode) // 查找前一个
    {
      if(++i == index) 
      {
        break;
      }

      pNode = pNode->Next;
    }

    return pNode;
  }

  void RemoveNext(Link_type pNode)
  {
   if(pNode->Next)
    {
      Link_type pNextNext = pNode->Next->Next; 

      delete pNode->Next;
      pNode->Next = pNextNext;
      --_size;
    }
  }

private:
  Link_type pHeadNode; // 头节点指针
  unsigned int _size;  // 链表节点个数
};

测试代码: main.h

#include "single_list.h"

int main(int argc, char * argv[])
{
    T_Single_List link_list;
    
    // 前插
    link_list.InsertValueAtHead(1);
    link_list.InsertValueAtHead(2);
    link_list.InsertValueAtHead(3);
    link_list.InsertValueAtHead(4);
    link_list.InsertValueAtHead(5);

    std::cout << "The link_list:";
    link_list.printf();

    T_Single_List link_list1;

    // 尾插
    link_list1.InsertValueAtTail(1);
    link_list1.InsertValueAtTail(2);
    link_list1.InsertValueAtTail(3);
    link_list1.InsertValueAtTail(4);
    link_list1.InsertValueAtTail(5);

    std::cout << "The link_list1:";
    link_list1.printf();

    std::cout << "get link_list The third node: " << link_list.GetValueByIndex(3) << std::endl;

    std::cout << "get link_list The RE sec node: " << link_list.GetRePosNodeValue(2) << std::endl;

    std::cout << "get link_list The Mid node: " << link_list.GetMidNode()->dataEntry << std::endl;

    std::cout << "get link_list1 The third node: " << link_list1.GetValueByIndex(3) << std::endl;

    std::cout << "get link_list1 The RE sec node: " << link_list1.GetRePosNodeValue(2) << std::endl;

    std::cout << "get link_list1 The Mid node: " << link_list1.GetMidNode()->dataEntry << std::endl;


    std::cout << "The RE link_list:";
    link_list.ReverseList();
    link_list.printf();

    std::cout << "The RE link_list1:";
    link_list1.ReverseList();
    link_list1.printf();


    std::cout << "GetValueByIndex:";

    std::cout << "get link_list1 The second node: " << link_list1.GetValueByIndex(2) << std::endl;

    std::cout << "update:";

    link_list1.update(2, 100);

    link_list1.deleteValueAtIndex(1);

    link_list1.printf();

    link_list.clear();

    link_list1.clear();

    link_list.printf();

    link_list1.printf();

    return 0;
}

测试结果:

bash-4.2$ ./single_list 
The link_list:5 4 3 2 1 
The link_list1:1 2 3 4 5 
get link_list The third node: 3
get link_list The RE sec node: 2
get link_list The Mid node: 3
get link_list1 The third node: 3
get link_list1 The RE sec node: 4
get link_list1 The Mid node: 3
The RE link_list:1 2 3 4 5 
The RE link_list1:5 4 3 2 1 
GetValueByIndex:get link_list1 The second node: 4
update:4 3 100 1 

4. 单链表热门问题

单链表定义:

 // Definition for singly-linked list.
 struct ListNode {
     int val;
     ListNode *next;
     ListNode(int x) : val(x), next(NULL) {}
  };

1. 合并两个有序链表

解决思路类似于归并排序,边界情况,主要注意其中一个链表为空及两个链表都为空的情况,空间复杂度O(1),时间复杂度O(max(len1, len2))

实现代码

``` ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { if(l1 == NULL) { return l2; }
if(l2 == NULL)
{
    return l1;
}
    
ListNode newNode(-1);
ListNode* pNode = &newNode;
    
while(l1 || l2)
{
    if(l1 == NULL)
    {
        pNode->next = l2;
        break;
    }
        
    if(l2 == NULL)
    {
        pNode->next = l1;
        break;
    }
        
    if(l1->val > l2->val)
    {
        ListNode* pTemp = l2;
        l2 = l2->next;
            
        pTemp->next = pNode->next;
        pNode->next = pTemp;
        pNode = pNode->next;
    }
    else
    {
        ListNode* pTemp = l1;
        l1 = l1->next;
            
        pTemp->next = pNode->next;
        pNode->next = pTemp;
        pNode = pNode->next;
    }
}
    
return newNode.next;

}


<p style="font-size: 15px;font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">2. 查找链表的中间结点</p>
<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';"> 设置两个指针,两个指针同时向前走,一个走两步,一个走一步,当快指针到达链表末尾时,慢指针刚好到中间,刚好可以得到中间节点。空间复杂度<font color="#ff0000">O(1)</font>,时间复杂度<font color="#ff0000">O(n)</font></p>

<p style="font-size: 15px;font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">实现代码</p>

ListNode* middleNode(ListNode* head) {
ListNode* pNodeStep1 = head; // slow 慢指针
ListNode* pNodeStep2 = head; // fast 快指针

while(pNodeStep2 && pNodeStep2->next)
{
    pNodeStep2 = pNodeStep2->next->next;
    pNodeStep1 = pNodeStep1->next;       
}
    
return pNodeStep1;   

}


<p style="font-size: 15px;font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">3. 求两个单链表相交的第一个节点</p>
<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';"> 首先,我们遍历一下两个链表获取链表长度lengthA,lengthB。然后对于长度较长的链表,先向前走|lengthA-lengthB|的步数,接着两个链表一起往前走,直到找到第一个地址相同的节点,任务完成。空间复杂度<font color="#ff0000">O(1)</font>,时间复杂度<font color="#ff0000">O(n)</font></p>

<p style="font-size: 15px;font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">实现代码</p>

ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(headA == NULL || headB == NULL)
{
return NULL;
}

int lengthA = 0;
int lengthB = 0;

ListNode *travelNodeA = headA;
ListNode *travelNodeB = headB; 

while(travelNodeA)
{
    ++lengthA;
    travelNodeA = travelNodeA->next;
}

while(travelNodeB)
{
    ++lengthB;
    travelNodeB = travelNodeB->next;
}

travelNodeA = headA;
travelNodeB = headB; 

// offset = |lengthB - lengthA|
int offset;
if(lengthB > lengthA)
{
    offset = lengthB - lengthA;
    
    while(offset > 0)
    {
        travelNodeB = travelNodeB->next;
        --offset;
    }
}
else
{
    offset = lengthA - lengthB;
    
    while(offset > 0)
    {
        travelNodeA = travelNodeA->next;
        --offset;
    }
}


while(travelNodeA)
{
    if(travelNodeA == travelNodeB)
    {
        return travelNodeA;
    }
    
    travelNodeA = travelNodeA->next;
    travelNodeB = travelNodeB->next;
}

return NULL;

}


<p style="font-size: 15px;font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">4. 给出一单链表头指针pHead和一节点指针pToBeDeleted,O(1)时间复杂度删除节点pToBeDeleted</p>
<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';"> 主要思路,对于单链表,想要删除某个节点,那我们必须知道他的前一个节点,然后通过改变前一个节点的next指针来将该节点删除,而又因为链表中保存的dataEntry结构是一样的 ,都为整形,因此我们可以将该节点和他的后一个节点数据进行交换,使得该节点变成前一个节点,接着通过删除后一个节点的方式,即完成了该节点的删除。空间复杂度<font color="#ff0000">O(1)</font>,时间复杂度<font color="#ff0000">O(1)</font></p>

<p style="font-size: 15px;font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">实现代码</p>

void deleteNode(ListNode* node) {
if(node == NULL || node->next == NULL)
{
return;
}

ListNode* pNode = node->next;

node->val = pNode->val;    
node->next = pNode->next;

delete pNode;
pNode = NULL;

}


<p style="font-size: 15px;font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">5. 链表成环的问题</p>

<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">1. 判断一个链表中是否有环存在</p>
<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';"> 主要思路,也是使用两个指针去遍历,一个指针一次走两步,一个指针一次走一步,如果有环,两个指针肯定会在环中相遇。空间复杂度<font color="#ff0000">O(1)</font>,时间复杂度<font color="#ff0000">O(1)</font></p>

<p style="font-size: 15px;font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">实现代码</p>

bool hasCycle(ListNode *head) {
if(head == NULL || head->next == NULL)
{
return false;
}

ListNode *step1Node = head; // slow
ListNode *step2Node = head; // fast

while(step2Node && step2Node->next)
{
    step2Node = step2Node->next->next;
    step1Node = step1Node->next;
    
    if(step2Node == step1Node)
    {
        return true;
    }
}

return false;

}

------

<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">2. 如果存在环,找出环的入口点;</p>
![链表成环示意图](https://www.cnblogs.com/images/cnblogs_com/blog-yejy/1281855/o_71135105-file_1483521194130_8c39.png)

<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">对于链表成环的问题,最关键的一点就在于如何获取到环的入口点,如果获取到了,那关于环的各种信息,都可以很容易的得到,那么,我们来分析一下入口节点的获取思路</p>
<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">我们使用快慢节点可以知道链表是否有环,因为快慢节点一直往前走的话,是肯定会相遇的,但是具体相遇在哪里,入口点是哪里呢,<font color="#ff0000">首先我们可以确定的一点是,慢指针在跑完一圈的过程中,快指针是肯定会和慢指针相遇的</font>,因为快指针速度是慢指针的两倍,想象一下如果所有节点都在环内,那就和跑圈是一样的了,跑圈最终是在起点的地方相遇,示意图则是过程中相遇。</p>
<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">那么我们在这个前提下进行分析,根据示意图,我们假设图中 <font color="#ff0000">m1</font> 为相遇点,那慢节点走过的距离为 <font color="#ff0000">a+b</font>,这个时候快节点绕圈多少次呢,我们不知道,假设为 <font color="#ff0000">n</font> 次,环一周距离为 <font color="#ff0000">k</font>,那我们可以得出这个等式:</p>

<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';"><font color="#ff0000">(a + b) * 2 = a + b + n * k     (慢指针跑过距离的两倍等于快指针跑过的距离)</font></p>

<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">我们对等式变一下形,因为我们要求入口节点,那么首先需要得到 <font color="#ff0000">a</font>   的等式分析一下</p>

<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';"><font color="#ff0000">a = n * k - b  </font></p>

<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">好像还是看不出来,只知道 <font color="#ff0000">a+b</font> 刚好等于一圈的 <font color="#ff0000">n</font> 倍,我们再换个样子看一下</p>

<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';"><font color="#ff0000">a = (n-1) * k  + (k - b)  </font></p>

<p style="font-size: 15px; font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">那现在呢,因为我们链表是顺一个方向走的,而 <font color="#ff0000">a</font> 的距离刚好是从头节点到入口点的距离,而 <font color="#ff0000">k - b</font>,刚好是相遇点,走到入口点的距离,因为我们在实现链表是否成环中已经找到了相遇点,那么我们改变一下思路,<font color="#ff0000">我们不走两步,走一步,让一个指针从相遇点开始一步一步往前走,让另一个指针从头节点开始一步一步往前走,最后是不是在入口点就相遇了,只是从相遇点开始走的那个指针可能是多次经过了入口点而已。</font></p>

<p style="font-size: 15px;font-weight: bold;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">实现代码</p>

ListNode *detectCycle(ListNode *head) {
if(head == NULL || head->next == NULL)
{
return NULL;
}

ListNode *step1Node = head; // slow
ListNode *step2Node = head; // fast

while(step2Node && step2Node->next)
{
    step2Node = step2Node->next->next;
    step1Node = step1Node->next;

    if(step2Node == step1Node)
    {
        ListNode *startNode = head;
        ListNode *meetNode = step1Node;
        while(startNode)
        {   
            if(meetNode == startNode)
            {
                return meetNode;
            }
            startNode = startNode->next;
            meetNode = meetNode->next;
        }
    }
}

  return NULL;

}


<p style="font-size: 15px;text-indent:2em;letter-spacing:1px; font-family: '微软雅黑';">实现都是这两天在leetcode上刷的,有关链表的问题上面基本都有,如果想要锻炼一下的话,可以去上面尝试一下!! </p>
<p style="font-size: 15px;text-indent:45em;letter-spacing:1px; font-family: '微软雅黑';"> 2018年8月25日23:16:37 </p>
原文地址:https://www.cnblogs.com/blog-yejy/p/9508114.html