赞
踩
继续造火箭。
什么是链表?顾名思义,链表就是链状的数据结构,可以想象成在日常生活中,一条铁链一环扣一环的链结在一起。每个环上存储了相关的数据。在数据结构中,组成链表的节点通常有两个以上的域,一个域用来存储数据,其它的域用来存储指向其它节点的方式(指针等)。这样通过每个节点的指向其它节点的方式形成一条数据链。
链表可以有头节点,也可以没有头节点,但一定会有尾节点(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)才能得到长度大小。
链表在面试中经常遇到的主要是转置和查找环,这是基本需要掌握的。至于链表的建立,只要记住尾部插入基本就没有问题。头部入在实际工程中经常遇到,因为头节点不需要有专门的指针来保存,头节点始终是头节点,更容易维护。至于其它的,都属于比较高的要求了,达到更好,达不到一般也不会有太大影响。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。