【Algo】链表 Linked List

Backto Algo List


Linked ListArray 是亲兄弟, 但是性格秉性完全相反, 各有大用.
二者都是在内存空间上顺序排列相同的元素, 最本质的区别是 Array 需要一块儿连续的内存, 而 Linked List 则不需要内存联系, 通过指针把零散的内存空间组织起来.

在这里插入图片描述

单链表: 最简单的链表实现

在这里插入图片描述

Linked List 中的每个节点都由 data 数据next 指针 两部分组成.

  • 第一个节点叫 head, 也是 Linked List 的起始地址
  • 最后一个节点叫 tail, tail 的 next 指针指向 NULL, 代表 Linked List 结束

循环链表: 首尾相连

循环链表就是 tail 的 next 指针指向 head, 从而可以很方便的从 tail 访问 head 数据. 这种数据结构非常适合于具有环形结构的数据, 比如 约瑟夫问题.

双向链表: 前后兼顾

在这里插入图片描述

很多情况下, 我们不仅需要知道一个节点的下一个节点 next, 还需要知道上一个节点 previous. 对于单向列表, 找 prev 需要从 head 开始遍历比较, 复杂度 O ( n ) O(n) O(n), 当这一操作很频繁的时候耗时严重, 于是引入了双向链表. 但是双向链表因为每个节点都增加了一个 prev 指针, 所以占用内存空间会更多. 所以, 这就面临一种 时间-空间 上的技术选择

  • 空间比较紧张, 就 用时间换空间, 选择单向列表
  • 空间比较充足, 就 用空间换时间, 选择双向列表

在现实开发中, 除了嵌入式开发等对内存严格要求的环境, 一般空间都是充足的, 所以 双向链表是应用最广泛的.

双向循环链表: 前后兼顾, 首尾相连

在这里插入图片描述
还想要啥? 通通给你.

复杂度分析

理论上, Array 和 Linked List 常用操作的 average 时间复杂度如下表

Data StructureAccessSearchInsertionDeletion
Array O ( 1 ) O(1) O(1) O ( n ) O(n) O(n) O ( n ) O(n) O(n) O ( n ) O(n) O(n)
Linked List O ( n ) O(n) O(n) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1)

注意, 这里指的是理论上的所有信息都透明的原子操作.
但是, 在实际业务场景中, 情况就变得复杂了, 不然也不会衍生出那么多不同的 Linked List.
比如, Linked List 中理论上 Insertion 和 Deletion 的复杂度都是 O ( 1 ) O(1) O(1), 但是在实际业务中, 给定的场景往往是

  1. 给定一个定值 value, 把 data == value 的节点删除, 或者在这个节点之前之后insertion
  2. 给定一个节点的 pointer, 把这个节点 delete 或者 在前在后 insertion

对于第一个场景,首先需要先找到这个节点, search 就是一个 O ( n ) O(n) O(n) 操作了。对于第二种操作, 若在给定 pointer 的节点之前 insertion,Singly-Linked List 就又要从 head 开始去 search, 复杂度 O ( n ) O(n) O(n),而对于 Doubly-Linked List 而言,因为有 prev 指针,就是一个简单的 O ( 1 ) O(1) O(1)操作。

单链表问题汇总 singly-linked list

  • 定义链表
#typedef int ElemType // for easy implementation

typedef struct LNode { 
	ElemType data; 
	struct LNode *next;
} LNode, *LinkedList; // LinkedList is type of (struct LNode*)
  • 判断链表是否为空
bool IsEmpty(LinkedList *head) {
	return head == NULL;
}
  • 打印链表
void PrintLinkedList(LinkedList *head) {
	if(!IsEmpty(head)) {
		LNode *iter = *head;
		int idx = 0;
		while(iter) {
			printf("[%02d]: %08d\n", idx++, iter->val);
			iter = iter->next;
		}
	}	
}
  • 给定数组, 从head 开始初始化 list
LinkedList*  InitListFromHeadWithArray(const int* arr, const int& arr_length) {
	if(arr == NULL || arr_length == 0)
		return NULL;
	LinkList head =(LinkList)malloc(sizeof(LNode)); // always stands for the head
	head->next = NULL;
	head->data = arr_length; // head node stands for the length of elems
	for(int i = 0; i < arr_length; ++i) {
		LNode* node = (LNode*)malloc(sizeof(LNode));
		node ->data = arr[i];
		node ->nex= head;
		head->next= node;	
	}
	return &head;
}
  • 给定位置插入 node
void Insert(LinkedList *pos, LNode *elem) {
	if(IsEmpty(pos) || elem == NULL)
		return;
	
	if(&pos)
}
  • 从头部插入node
  • 从尾部插入node
  • 从任意位置插入 node
  • 删除
  • 反转链表
  • 链表排序
  • 找到中间值

业务场景

Scenario 01 : LRU 缓存淘汰算法

缓存(Cache) 是一种提高数据读取性能的技术, 但 Cache 制造成本高, 空间大小很受限. 因此其核心问题有两个 有free space 的时候, 哪些数据被 put in?space 被占满后, 哪些数据 被 wipe out?. 针对后一个问题的解决方案, 就是 缓存淘汰策略, 常用的策略有三种

  1. FIFO(First-In First-Out)
  2. LFU(Least Frequently Used)
  3. LRU(Least Recently Used)

其中, LRU 用一个 Ordered Singly-Linked List 来实现就很容易, Ordered 的排序就是越靠近 head 的就是越 recently 被访问的. 当有一个 new data 需要被访问的时候, 我们就开始遍历这个 list, 找到了(已经在Cache 里了), 就把它原地删除挪到 head 来. 如果没找到, 就把 tail 删除, 把 new data 放在 head 的位置. 当然这个思路是 O ( n ) O(n) O(n) 的, 在 Cache 这么对性能要求严苛的场景一般是不会采用的. 继续优化的话, 可以采用 散列表 Hash table 记录每个数据的位置, 从而将缓存访问的时间复杂度降为 O ( 1 ) O(1) O(1).


Ref

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值