链表:通过“指针”将一组零散的内存块串联起来使用。
数组vs链表

三种常见的链表:单链表、双向链表、循环链表。
单链表:

时间复杂度:
- 插入和删除:O(1)
- 随机访问:O(n)
循环链表:

一种特殊的单链表,跟单链表唯一的区别在尾结点:单链表尾结点是Null、循环链表尾结点指向头结点。
和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。
时间复杂度:
- 插入和删除:O(1)
- 随机访问:O(n)
双向链表:

通过额外空间来存储后继结点和前驱结点的地址。正是这种特点,使得双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。这里有个更加重要的知识点:用空间换时间。
双向循环链表:

链表VS数组性能大比拼

数据结构 | 优点 | 缺点 |
---|---|---|
数组 | 简单易用; 可借助CPU的缓存机制; 访问效率更高; |
大小固定; |
链表 | 大小没限制,支持动态扩容 | 对CPU缓存不好友,无法预读取; |
如何实现LRU缓存淘汰算法
维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,从链表头开始顺序遍历链表。
- 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
- 如果此数据没有在缓存链表中,又可以分为两种情况:
- 如果此时缓存未满,则将此结点直接插入到链表的头部;
- 如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
课后思考
如果字符串是通过单链表来存储的,那该如何来判断是一个回文串呢?时间、空间复杂度是多少?
namespace PalindromeApp
{
class Program
{
static void Main(string[] args)
{
string textA = "abcdcba";
Console.WriteLine("{0}是否回文:{1}", textA, IsPalindrome(Init(textA)));
string textB = "abcddcba";
Console.WriteLine("{0}是否回文:{1}", textB, IsPalindrome(Init(textB)));
string textC = "abedcba";
Console.WriteLine("{0}是否回文:{1}", textC, IsPalindrome(Init(textC)));
Console.Read();
}
static MyLinkedNode Init(string text)
{
MyLinkedNode head = new MyLinkedNode('&');
MyLinkedNode curr = head;
foreach (char c in text)
{
curr.Next = new MyLinkedNode(c);
curr = curr.Next;
}
return head;
}
static MyLinkedNode Reverse(MyLinkedNode head)
{
MyLinkedNode reversed = null;
MyLinkedNode curr = head.Next;
while (curr != null)
{
MyLinkedNode node = new MyLinkedNode(curr.Data);
node.Next = reversed;
reversed = node;
curr = curr.Next;
}
MyLinkedNode reversedHead = new MyLinkedNode('&');
reversedHead.Next = reversed;
return reversedHead;
}
static bool IsPalindrome(MyLinkedNode head)
{
MyLinkedNode reversed = Reverse(head);
Show(head);
Show(reversed);
MyLinkedNode curr = head.Next;
MyLinkedNode reversedCurr = reversed.Next;
while (curr != null && reversedCurr != null)
{
if (curr.Data != reversedCurr.Data)
{
return false;
}
curr = curr.Next;
reversedCurr = reversedCurr.Next;
}
if (curr != null || reversedCurr != null)
{
return false;
}
return true;
}
static void Show(MyLinkedNode head)
{
MyLinkedNode curr = head.Next;
string msg = "序列:";
while (curr != null)
{
if (curr.Data == -1)
{
break;
}
msg += curr.Data;
curr = curr.Next;
}
Console.WriteLine(msg);
}
}
public class MyLinkedNode
{
public MyLinkedNode(char data)
{
Data = data;
}
public char Data { get; set; }
public MyLinkedNode Next { get; set; }
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
写链表代码六个技巧
**技巧一:理解指针或引用的含义 **
指针:将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
技巧二:警惕指针丢失和内存泄漏
插入结点时,一定要注意操作的顺序;删除链表结点时,也一定要记得手动释放内存空间(对于有自动管理内存的编程语言来说,就不需要考虑这么多了)
技巧三:利用哨兵简化实现难度
针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。

技巧四:重点留意边界条件处理
检查点:
- 如果链表为空时,代码是否能正常工作?
- 如果链表只包含一个结点时,代码是否能正常工作?
- 如果链表只包含两个结点时,代码是否能正常工作?
- 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
技巧五:举例画图,辅助思考
技巧六:多写多练,没有捷径
5个常见的链表操作
1、单链表反转
解题思路:遍历结点,把当前结点插入到头结点,并对受影响的结点(前结点、原头部结点等)的Next重新设置。
public class ReversedLinkedList
{
public static void Run()
{
// 空链表
MyLinkedNode empty = Init(new int[] { });
Console.WriteLine("初始链表:");
Show(empty);
Console.WriteLine("反转链表:");
Show(Reversed(empty));
// 单结点
MyLinkedNode single = Init(new int[] { 1 });
Console.WriteLine("初始链表:");
Show(single);
Console.WriteLine("反转链表:");
Show(Reversed(single));
// 正常链表
MyLinkedNode normal = Init(new int[] { 1, 2, 3, 4, 5, 6, 7 });
Console.WriteLine("初始链表:");
Show(normal);
Console.WriteLine("反转链表:");
Show(Reversed(normal));
}
/// <summary>
/// 初始化链表
/// </summary>
/// <param name="datas"></param>
/// <returns></returns>
public static MyLinkedNode Init(int[] datas)
{
MyLinkedNode head = new MyLinkedNode(-1);
MyLinkedNode curr = head;
for (int i = 0; i < datas.Length; i++)
{
var node = new MyLinkedNode(datas[i]);
curr.Next = node;
curr = node;
}
return head;
}
public static MyLinkedNode Reversed(MyLinkedNode head)
{
if (head == null || head.Next == null)
{
return head;
}
// 首结点不用操作,从第二个结点开始
MyLinkedNode curr = head.Next.Next;
MyLinkedNode prev = head.Next;
while (curr != null)
{
MyLinkedNode node1 = head.Next;
MyLinkedNode node2 = curr.Next;
// 将当前结点插入到头结点
head.Next = curr;
// 将当前结点的Next设置为原来的头结点,插入头结点完成
curr.Next = node1;
// 前结点的Next设置为当前结点的原Next,把当前结点的后续结点链接起来
prev.Next = node2;
// 当前结点移动到下一结点
curr = prev.Next;
}
return head;
}
public static void Show(MyLinkedNode head)
{
MyLinkedNode curr = head.Next;
string msg = "序列:";
while (curr != null)
{
msg += string.Format("{0} ", curr.Data);
curr = curr.Next;
}
Console.WriteLine(msg);
}
public class MyLinkedNode
{
public MyLinkedNode(int data)
{
Data = data;
}
public int Data { get; set; }
public MyLinkedNode Next { get; set; }
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2、链表中环的检测
解题思路:设置两个步长,分别+1和+2,当出现结点相同时,表示有环。
类比,有两个跑步运动员A和B,分别以1m/s和2m/s的速度在同一个跑道上跑步,当B能追上A时,表示跑道是环,如果B不能追上A,表示非环。
public class CircleLinkedList
{
public static void Run()
{
// 空链表
MyLinkedNode empty = Init(new int[] { }, false);
Console.WriteLine("是否环:{0}", IsCircle(empty));
// 单结点
MyLinkedNode single = Init(new int[] { 1 }, false);
Console.WriteLine("是否环:{0}", IsCircle(single));
// 非环
MyLinkedNode notcircle = Init(new int[] { 1, 2, 3, 4, 5 }, false);
Console.WriteLine("是否环:{0}", IsCircle(notcircle));
// 环
MyLinkedNode circle = Init(new int[] { 1, 2, 3, 4, 5 }, true);
Console.WriteLine("是否环:{0}", IsCircle(circle));
}
/// <summary>
/// 初始化链表
/// </summary>
/// <param name="datas"></param>
/// <returns></returns>
public static MyLinkedNode Init(int[] datas, bool circle)
{
MyLinkedNode head = new MyLinkedNode(-1);
MyLinkedNode curr = head;
//int[] datas = new int[] { 1, 2, 3, 4, 5 };
for (int i = 0; i < datas.Length; i++)
{
var node = new MyLinkedNode(datas[i]);
curr.Next = node;
curr = node;
}
if (circle)
{
curr.Next = head.Next;
}
return head;
}
public static bool IsCircle(MyLinkedNode head)
{
// 当链表为空或者只有一个结点时,返回false
if (head == null || head.Next == null || head.Next.Next == null)
{
return false;
}
MyLinkedNode stepA = head.Next;
MyLinkedNode stepB = head.Next.Next;
while (stepA != null && stepB != null)
{
if (stepA == stepB)
{
return true;
}
// 设置步长+1
stepA = stepA.Next;
// 当下一下结点为Null,结束循环,跳出
if (stepB.Next == null)
{
break;
}
else
{
// 设置步长+2
stepB = stepB.Next.Next;
}
}
return false;
}
public static void Show(MyLinkedNode head)
{
MyLinkedNode curr = head.Next;
string msg = "序列:";
while (curr != null)
{
msg += string.Format("{0} ", curr.Data);
curr = curr.Next;
}
Console.WriteLine(msg);
}
public class MyLinkedNode
{
public MyLinkedNode(int data)
{
Data = data;
}
public int Data { get; set; }
public MyLinkedNode Next { get; set; }
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
3、两个有序的链表合并
解题思路:遍历两个链表,把结点往新链表尾部插入。当某个链表遍历完,把另一个链表直接插入到新链表的尾部。
/// <summary>
/// 两个有序的链表合并
/// </summary>
public class MergeLinkedList
{
public static void Run()
{
// 两个为空链表
MyLinkedNode emptyA = Init(new int[] { });
MyLinkedNode emptyB = Init(new int[] { });
Show(Merge(emptyA, emptyB));
// 一个为空链表
MyLinkedNode emptyC = Init(new int[] { });
MyLinkedNode linkedD = Init(new int[] { 1, 2, 4, 5, 6 });
Show(Merge(emptyC, linkedD));
// 两个非空链表
MyLinkedNode linkedE = Init(new int[] { 1, 3, 4, 5, 9, 10 });
MyLinkedNode linkedF = Init(new int[] { 2, 3, 5, 6, 7, 8 });
Show(Merge(linkedE, linkedF));
}
/// <summary>
/// 初始化链表
/// </summary>
/// <param name="datas"></param>
/// <returns></returns>
private static MyLinkedNode Init(int[] datas)
{
MyLinkedNode head = new MyLinkedNode(-1);
MyLinkedNode curr = head;
foreach (var data in datas)
{
var node = new MyLinkedNode(data);
curr.Next = node;
curr = node;
}
return head;
}
private static MyLinkedNode Merge(MyLinkedNode headA, MyLinkedNode headB)
{
if (headA == null || headA.Next == null)
{
return headB;
}
if (headB == null || headB.Next == null)
{
return headA;
}
MyLinkedNode head = new MyLinkedNode(-1);
MyLinkedNode curr = head;
MyLinkedNode currA = headA.Next;
MyLinkedNode currB = headB.Next;
while (currA != null && currB != null)
{
if (currA.Data <= currB.Data)
{
curr.Next = currA;
currA = currA.Next;
}
else
{
curr.Next = currB;
currB = currB.Next;
}
curr = curr.Next;
curr.Next = null;
}
curr.Next = currA ?? currB;
return head;
}
private static void Show(MyLinkedNode head)
{
MyLinkedNode curr = head.Next;
string msg = "序列:";
while (curr != null)
{
msg += string.Format("{0} ", curr.Data);
curr = curr.Next;
}
Console.WriteLine(msg);
}
public class MyLinkedNode
{
public MyLinkedNode(int data)
{
Data = data;
}
public int Data { get; set; }
public MyLinkedNode Next { get; set; }
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
4、删除链表倒数第n个结点
解题思路:反转链表,遍历到第n-1个结点,链接到第n+1个结点(即删除第n个结点),再反转链表。
public class RemoveLinkedList
{
public static void Run()
{
// 删除第4个结点,即下标是3的结点
int index = 3;
// 空链表
MyLinkedNode empty = Init(new int[] { });
Console.Write("初始链表");
Show(empty);
Console.WriteLine("删除倒数第{0}个结点:{1}", index + 1, RemoveAt(empty, index));
Show(empty);
// 单结点
MyLinkedNode single = Init(new int[] { 1 });
Console.Write("初始链表");
Show(single);
Console.WriteLine("删除倒数第{0}个结点:{1}", index + 1, RemoveAt(single, index));
Show(single);
// 正常链表
MyLinkedNode normal = Init(new int[] { 1, 2, 3, 4, 5, 6, 7 });
Console.Write("初始链表");
Show(normal);
Console.WriteLine("删除倒数第{0}个结点:{1}", index + 1, RemoveAt(normal, index));
Show(normal);
}
/// <summary>
/// 初始化链表
/// </summary>
/// <param name="datas"></param>
/// <returns></returns>
private static MyLinkedNode Init(int[] datas)
{
MyLinkedNode head = new MyLinkedNode(-1);
MyLinkedNode curr = head;
for (int i = 0; i < datas.Length; i++)
{
var node = new MyLinkedNode(datas[i]);
curr.Next = node;
curr = node;
}
return head;
}
private static bool RemoveAt(MyLinkedNode head, int index)
{
var reversed = Reversed(head);
int count = 0;
MyLinkedNode curr = reversed.Next;
while (curr != null)
{
count++;
if (count == index)
{
curr.Next = curr.Next.Next;
break;
}
curr = curr.Next;
}
head = Reversed(reversed);
return count >= index;
}
private static MyLinkedNode Reversed(MyLinkedNode head)
{
if (head == null || head.Next == null)
{
return head;
}
// 首结点不用操作,从第二个结点开始
MyLinkedNode curr = head.Next.Next;
MyLinkedNode prev = head.Next;
while (curr != null)
{
MyLinkedNode node1 = head.Next;
MyLinkedNode node2 = curr.Next;
// 将当前结点插入到头结点
head.Next = curr;
// 将当前结点的Next设置为原来的头结点,插入头结点完成
curr.Next = node1;
// 前结点的Next设置为当前结点的原Next,把当前结点的后续结点链接起来
prev.Next = node2;
// 当前结点移动到下一结点
curr = prev.Next;
}
return head;
}
private static void Show(MyLinkedNode head)
{
MyLinkedNode curr = head.Next;
string msg = "序列:";
while (curr != null)
{
msg += string.Format("{0} ", curr.Data);
curr = curr.Next;
}
Console.WriteLine(msg);
}
public class MyLinkedNode
{
public MyLinkedNode(int data)
{
Data = data;
}
public int Data { get; set; }
public MyLinkedNode Next { get; set; }
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
5、求链表的中间结点
解题思路:遍历一次,计算链表元素个数,然后算出中间结点的下标,转为获取第N个结点。
public class MiddleLinkedList
{
public static void Run()
{
MyLinkedNode middleNode = null;
// 空链表
MyLinkedNode empty = Init(new int[] { });
Console.Write("初始链表");
Show(empty);
middleNode = GetMiddleNode(empty);
Console.WriteLine("中间结点:{0}", middleNode == null ? "null" : middleNode.Data.ToString());
// 单结点
MyLinkedNode single = Init(new int[] { 1 });
Console.Write("初始链表");
Show(single);
middleNode = GetMiddleNode(single);
Console.WriteLine("中间结点:{0}", middleNode == null ? "null" : middleNode.Data.ToString());
// 单数链表
MyLinkedNode odd = Init(new int[] { 1, 2, 3, 4, 5, 6, 7 });
Console.Write("初始链表");
Show(odd);
middleNode = GetMiddleNode(odd);
Console.WriteLine("中间结点:{0}", middleNode == null ? "null" : middleNode.Data.ToString());
// 双数链表
MyLinkedNode even = Init(new int[] { 1, 2, 3, 4, 5, 6, 7, 8 });
Console.Write("初始链表");
Show(even);
middleNode = GetMiddleNode(even);
Console.WriteLine("中间结点:{0}", middleNode == null ? "null" : middleNode.Data.ToString());
}
/// <summary>
/// 初始化链表
/// </summary>
/// <param name="datas"></param>
/// <returns></returns>
private static MyLinkedNode Init(int[] datas)
{
MyLinkedNode head = new MyLinkedNode(-1);
MyLinkedNode curr = head;
for (int i = 0; i < datas.Length; i++)
{
var node = new MyLinkedNode(datas[i]);
curr.Next = node;
curr = node;
}
return head;
}
private static MyLinkedNode GetMiddleNode(MyLinkedNode head)
{
if (head == null || head.Next == null)
{
return null;
}
int count = 0;
MyLinkedNode curr = head.Next;
while (curr != null)
{
count++;
curr = curr.Next;
}
curr = head;
int middleIndex = count == 1 ? 0 : (count - 1) / 2;
while (middleIndex >= 0)
{
middleIndex--;
curr = curr.Next;
}
//for (int i = 0; i <= middleIndex; i++)
//{
// curr = curr.Next;
//}
return curr;
}
private static void Show(MyLinkedNode head)
{
MyLinkedNode curr = head.Next;
string msg = "序列:";
while (curr != null)
{
msg += string.Format("{0} ", curr.Data);
curr = curr.Next;
}
Console.WriteLine(msg);
}
public class MyLinkedNode
{
public MyLinkedNode(int data)
{
Data = data;
}
public int Data { get; set; }
public MyLinkedNode Next { get; set; }
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
课后思考
今天我们讲到用哨兵来简化编码实现,你是否还能够想到其他场景,利用哨兵可以大大地简化编码难度?
哨兵有点像中间件的味道,在两个系统或模块之间的调用时,有时候通过中间件能够简化两边的技术实现差异。
网友评论