🔥 四种数据结构中,链表是较为复杂的。
数组
概念&特性
-
概念(数据结构意义上的):一种线性表数据结构,用一组连续的内存空间存储一组具有相同类型的数据。
-
特性
- 只有前后两个方向
- 随机访问
-
一维数组寻址公式
-
javascript 中的数组
- 数组被改的面目全非了,与数据结构中的数组行为不一致。
- 不一定连续,支持变长数组
- 提供 ArrayBuffer ,这种数据结构符合标准数据结构的定义
时间复杂度
-
复杂度:根据下标随机访问元素的时间复杂度是O(1),插入删除的平均复杂度为 O(n)
-
插入、删除比较低效,因为数组的连续性
-
不考虑排序的话,插入指定位置,可以将时间复杂度降为 O(1)
不大规模移动,只将指定位置的元素放到队尾,新元素再放到该指定位置
-
🔥 删除的操作可以将多次删除操作一起执行,删除效率较高
标记清除
总结&思考
-
为什么数组下标从 0 开始?
-
数组暴露的缺点
- 数组需要一块连续的内存空间来存储,对内存要求比较高。当内存中没有连续的、足够大的存储空间时,会申请失败。
🔥 链表
应用
-
LRU缓存淘汰算法
常见的淘汰算法:先进先出(FIFO), 最少使用(LFY), 最近最少使用(LRU)
-
java LinkedHashMap 用到了双向链表
-
约瑟夫问题 循环链表
概念&特性
- 存储空间上不连续,利用指针(引用)来表示顺序
时间复杂度
-
访问第 k 个数据的时间复杂度为 O(n)
-
插入,删除操作(已知前驱节点的情况下)的时间复杂度为 O(1), 但需要先进行遍历查找其前驱节点。
-
双向链表可以解决插入、删除,需要遍历找出前驱节点的问题。将时间复杂度降低为 O(1)
链表的变形结构
-
循环链表
-
双向链表
-
双向循环链表
链表的奇淫技巧
-
理解指针和应用
-
警惕指针丢失和内存泄漏
-
🍅 利用 "哨兵" 简化代码
-
🍅 留意边界情况和特殊条件
- 链表空时
- 链表只有一个节点时
- 链表只有两个节点时
- 要处理的节点是头节点、尾节点时
-
画图
-
🍎 多多练习
- 单链表反转
- 链表中环的检测
- 两个有序的链表合并
- 删除链表倒数第 k 个节点
- 寻找链表中的中间节点
- LRU算法实现
思考&总结
-
双向链表空间换时间
-
链表 VS 数组
-
数组扩容麻烦,链表天然支持动态扩容
-
数组适合内存紧缺的开发场景
-
链表容易产生内存碎片
知识
-
LRU(Least recently used)缓存淘汰算法
根据数据的历史访问记录来进行淘汰数据,其核心思想是如果数据最近被访问过,那么将来被访问的几率也更高
训练题目
-
有红苹果标记的
-
基础的链表
栈
应用场景
-
浏览器前进后退
-
函数的调用栈
-
🍎 表达式求值
概念&特性
- 顺序栈数组实现
- 链式栈链表实现
时间复杂度
-
出栈、入栈都是 O(1)
-
🍎 动态扩容的入栈,最好 O(1) 最坏O(n) 均摊O(1)
训练题目
-
红苹果
-
括号匹配
-
链式栈
-
顺序栈
队列
概念&特性
- 先进先出
- 顺序队列数组实现
- 链式队列链表实现
时间复杂度
有一个问题需要注意,连个指针(tail, head)不断后移的过程中,tail移到最后,就没法入队了,此时需要,向前移动。
-
每次出队列都移动 O(n)
-
tail 在最右时整体移动 均摊O(1)
🍎 🔥 循环队列
-
关键:确定队列空和队列满的判定条件
-
当队列满的时候 head 和 tail 关系
-
tail 指针指向的位置,不存数据
阻塞队列和并发队列
-
阻塞队列实现 "生产者-消费者模型"
-
线程安全的队列又叫并发队列 上锁
-
基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的无锁并发队列,这也是循环队列比链式队列应用更广泛的原因
问题&思考
-
问题:当项固定大小的线程池请求一个线程时,如果线程池没有空闲资源,这个时候线程池如何处理这个请求
- 直接拒绝
- 阻塞队列 (数组实现有界排队队列,链表实现一个支持无界的排队队列)
🍎 练习
- 顺序队列
- 链式队列
- 循环队列