一、基础知识
继续造火箭。
什么是链表?顾名思义,链表就是链状的数据结构,可以想象成在日常生活中,一条铁链一环扣一环的链结在一起。每个环上存储了相关的数据。在数据结构中,组成链表的节点通常有两个以上的域,一个域用来存储数据,其它的域用来存储指向其它节点的方式(指针等)。这样通过每个节点的指向其它节点的方式形成一条数据链。
链表可以有头节点,也可以没有头节点,但一定会有尾节点(C/C++中,指向NULL的指针)。链表分为单向链表、双向链表和循环链表三类。这里以单向链表为例,双向链表和循环链表类似,只是多了一些辅助的指针,看上去有些复杂而已。
本文主要针对单向链表展开分析。
二、实现
单向链表的建立有两种形式,即头插法和尾插法两种:
1、正常建立(尾插法)
这是最常见的建立方法,把节点一个个插入到当前节点的尾部,形成一条链。
#include <iostream>
struct Node {
int d;
Node* next;
Node(int x) : d(x), next(nullptr)
{
}
};
// 尾插法
Node* CreateListTail(int len) {
if (len < 1)
{
return nullptr;
}
Node* first = new Node(100);
Node* pCur = first;
Node* pNode = nullptr;
int num = 1;
while (num <= len - 1)
{
pNode = new Node(num);
pCur->next = pNode;
pCur = pNode;
num++;
}
pCur->next = nullptr;
return first;
}
int main()
{
int len = 5;
Node*p = CreateListTail(len);
for (int num = 0; num < len; num++)
{
std::cout << p->d << " ";
p = p->next;
}
}
2、头插法
头插入的方法第一次是在阅读MFC的源码时看到的,大家可以参考稻田插秧,其实就是一种头部插入(从插秧人角度看),插秧人面向的始终是第一颗秧苗(最新插入的始终是头部)。
struct Node {
int d;
Node* next;
Node(int x) : d(x), next(nullptr)
{
}
};
// 头插法
Node* CreateListHead(int len) {
if (len < 1)
{
return nullptr;
}
Node* head = new Node(100);
head->next = nullptr;
Node* pNode = nullptr;
int num = 1;
while (num <= len - 1)
{
pNode = new Node(num);
pNode->next = head;
head = pNode;
num++;
}
return head;
}
int main()
{
int len = 5;
Node*p = CreateListHead(len);
for (int num = 0; num < len; num++)
{
std::cout << p->d << " ";
p = p->next;
}
}
三、应用
1、逆转的转置(逆向)
链表的转置有两种方法,即递归和就地转置。原地转置其实不复杂,主要是要理清楚三个指针的转换(所以又叫三指针法):
list* reverse(list*head)
{
list* pre = head;
list* cur = head->next;
list* tmp = head->next->next;
while (cur)
{
tmp = cur->next;
cur->next = pre;
pre=cur;
cur = tmp;
}
head->next = NULL;
return pre;
}
类似于把链表分成三个一组,每次都把当中节点的指向下一个节点的指针从原来指向后面的,改成指向前面的。同时将当中节点后移一位。
递归方法:
list* reverse(list* head)
{
if (head == NULL || head->next == NULL)
{
return head;
}
else
{
list* nhead = reverse(head->next);
head->next->next = head;
head->next = NULL;
return nhead;
}
}
2、环判断
链表中有没有环的是经常遇到的问题,判断的方法就是设置两个指针,一个指针一步步的前进,另外一个指针两步两步前进,如果二者相遇,则表示链表有环。即:
bool Circle(list* node)
{
list* fast = node, * slow = node;
while (fast != nullptr && fast->next != nullptr)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
return true;
}
return false;
}
3、入口判断
两个slow指针p1和p2,同时从交点和链表头出发,第一次相遇的位置即为入口.证明略。
4、环及非环长度
环长度是在环判断过程后,fast停止不动,slow继续前进,增加一个slow计数器,再次相遇后,计数器即为环长度。
int CircleLen(list* node)
{
list* fast = node, * slow = node;
bool has = false;
while (fast != nullptr && fast->next != nullptr)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
{
has = true;
break;
}
}
if (!has)
{
return 0;
}
int len = 0;
do
{
len++;
fast = fast->next->next;
slow = slow->next;
} while (fast != slow);
return len;
}
非环长度可以利用上面入口的判断,从链表指针头到入口即为非环的长度。
5、c++11
C++11中std::forward_list提供了一个单向链表的模板,应用更安全更便捷。但它仍然没有解决链表的根本问题,也就是随机性访问的问题,同时其出于于效率的考虑,没有提供类似于Size()的成员函数,导致其在使用时,需要进行std::distance(_begin, _end)才能得到长度大小。
四、总结
链表在面试中经常遇到的主要是转置和查找环,这是基本需要掌握的。至于链表的建立,只要记住尾部插入基本就没有问题。头部入在实际工程中经常遇到,因为头节点不需要有专门的指针来保存,头节点始终是头节点,更容易维护。至于其它的,都属于比较高的要求了,达到更好,达不到一般也不会有太大影响。