单链表
尾结点指针指向一个空地址NULL,表示这是链表上的最后一个结点。
循环链表
尾结点指针是指向链表的头结点。
约瑟夫问题
双向链表
每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
双向链表要比单链表占用更多的内存空间
Java 语言的 LinkedHashMap 容器
用空间换时间的设计思想
查找
因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。
需要 O(n) 的时间复杂度。
插入和删除操作
针对链表的插入和删除操作,只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)。
删除某个结点 q 需要知道其前驱结点,单链表删除操作需要 O(n) 的时间复杂度,而双向链表只需要 O(1) 的时间复杂度。
链表 VS 数组
对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片,如果是 Java 语言,就有可能会导致频繁的 GC(Garbage Collection,垃圾回收)
基于链表实现 LRU 缓存淘汰算法
思路:维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。
- 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
- 如果此数据没有在缓存链表中,又可以分为两种情况:如果此时缓存未满,则将此结点直接插入到链表的头部;如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
缓存访问的时间复杂度:不管缓存有没有满,都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 O(n)。
优化1:引入散列表(Hash table)来记录每个数据的位置,将缓存访问的时间复杂度降到 O(1)。
优化2:用数组来实现 LRU 缓存淘汰策略
判断一个字符串是否是回文字符串的问题
思路1:
1.对字符串s进行遍历,保留其中的字母和数字字符,放到另一个字符串sgood中;
2.使用字符串翻转API得到sgood的逆序字符串sgood_rev;
3.对比sgood和sgood_rev,只要字符串相同,sgood就是回文串。
思路2:
双指针,左右指针分别指向 sgood 的两侧
以上:时间复杂度O(n),空间复杂度O(n)
思路3:在原字符串上直接判断,使用双指针
时间复杂度O(n),空间复杂度O(1)