算法学习 (门徒计划)1-1 链表 学习笔记
前言
3月4日,开课吧门徒计划算法课第一讲学习笔记。
本课讲链表。
课题为:
1-1 链表以及经典问题
链表
链表概念简述
通常链表指单向链表,单向链表每一个最小单元包含2个参数,一个是存储的内容,另一个是指向下一个节点的内容。
也就是说,链表中的任何一个节点都能获取到其下一个节点(除非没有下一个节点)或者这个储存的内容。
java 方式链表节点举例:
public class ListNode {
int val;
ListNode next = null;
}
每个对象存储了一个内容和下一个对象。
链表特性
查询动作时间复杂度 n
由于链表是通过指针域的方式实现排布,因此当需要查询一个节点时,需要遍历该节点前所有节点,所以时间复杂度为o(n)。
插入和删除时间复杂度 1
当需要插入或者删除某个节点时,只需要对这个节点前后的指向关系进行修改,因此时间复杂度为o(1)。
链表场景应用场景
场景一:操作系统的动态内存分配
假设原始内存区域为10GB,现在使用malloc函数(一个动态分配内存的函数,这部分内存需要手动释放)分配了2GB内存,那么剩下的8GB内存就需要操作系统去进行维护。
而有一种操作系统维护内存的方式(openVPN中有一种基于链表的内存管理方案)就是通过将不同的剩余内存碎片穿成链表进行维护。
场景二:LRU缓存淘汰算法
LRU(Least Recently Used)(意思为近期最少使用)
举例:
设备间存在速度差异,可以通过将使用频率高的数据存放在高速设备上,来实现更高效率的调用。
其实现的方法为当容量到达上限时,删除(或转移)最少使用的,为新的数据留出空间。
经典问题
链表的反转
要求在不进行修改存储值的情况下,通过修改链表各个单元对于下一个单元的指向关系来翻转链表的元素顺序。
通用案例
要求将某个链表从第n个元素开始翻转k个长度
如图,n为3,k为3
思考解决方案:
首先除翻转段外,其余部分应保持不变,因此链表被分为3份,
- 翻转区块之前的一份:
顺序不变,最终末尾指向翻转区域的首位; - 翻转区块的一份:改变顺序,最终末尾指向翻转区块之后的首位;
- 翻转区块之后的一份: 顺序不变,不需要改动(不需要关注)
代码:
public static ListNode reverseLinkList(ListNode head, int n,int k) {
if(head == null || head.next == null || k < 2) return head;
//临时节点
ListNode temp,end;
//计算长度
int len = 0;
temp = head;
while (temp != null) {
len ++ ;
temp = temp.next;
end = temp; //记录结尾
}
//无法翻转
if(n+k>len) return head;
//创建空头,用于承接需要翻转的部分
ListNode dummy = new ListNode(0);
temp = head;
for(int i=0;i<n-1;i++){
temp = temp.next;
}
dummy.next = temp;
//创建调换时需要使用的中介变量
ListNode cur = temp;
for (int i = 1; i < k; i ++ ) {
//将下一个需要调换的提取出来
temp = cur.next;
//跨过下一个指向下下个
cur.next = temp.next;
//让提取的这个成为调换中新链表的头
temp.next = dummy.next;
dummy.next = temp;
}
return head;
}
旋转链表
要求将链表首位相接,向后走k位后将环拆开。
(代码略)
方案
取得尾部,指向头,以尾部所在指针向后走k步,然后取得next作为头的指针,将尾部所在指针指向null。return 头。
链表成环
对于单向链表,由于根据每一个节点只能获得下一个节点的位置,所以不能确保某一个节点不会指向其之前的节点。一旦发生这种情况,就会导致试图查询链表末尾的动作无法结束,并且无法查询到环以后的节点(假设原本有一段,那么因为没有被指向,这段将被丢弃)。
换句说法,一个节点如果可有2种方式到达,则链表成环,且该节点为入环点,但是链表每个元素只能知道指向什么,怎么知道这个点呢?
(本题困难主要来源于思路,代码十分简单,略过)
初级问题:判断某个链表是否成环
方案1 用额外空间 哈希表
提供一个足够大的空间,依次载入链表各个节点(不需要载入值,只载入下一个节点的连接关系),当一个点,被第二次有载入的需求时,这个链表就是有环的。
方案2 快慢指针
从头开始,提供2个指针,一个指针一次只向下找一位,另一个则试图找两位,持续这个动作直到快指针到达结尾或者两个指针在某一个节点相遇。因为在相同时间t内,快走过的路程一定是慢的2倍,在假定有环的情况下,如果一直走下去,二者一定都会进环,而在环内由于快的速度是慢的两倍,因此快一定能追上慢的指针。同理如果没有环,快指针一定会到达链表的结尾。
方案衍生:查找入环点。
额外空间对于入环点很好找,但是指针该怎么找呢?。
需要利用两个指针的速度差。
第一步: 记录两个指针第一次相遇的步数。
此时快指针领先慢指针的距离等于慢指针行走的距离(因为快指针速度是慢的两倍)。
第二步: 重新准备一个指针速度和慢指针相同,令这个指针从头开始走,同时慢指针继续走。
此时假设,三号指针(假定为C),一直走到快慢指针相遇的点,那么原来的慢指针将走到何处?
也会到达快慢指针的第一次相遇点(因为这个距离是快指针领先慢指针的距离)。
但是由于指针C和慢指针速度相同,且在环内会经过的路程,因此虽然可以确定二者在快慢指针的相遇点是重合的,但是有可能二者更早就相遇了(在环内同向同路程)。
第三步:新指针和慢指针持续走,直到第一次相遇。
新指针和慢指针相遇的点就是入环点。
而整个动作为新指针和慢指针在入环点相遇,然后共同以相同的速度,并行到达快慢指针的第一次相遇点。
衍生问题:判断某个数字是否是快乐数
判断某一个正整数是否是快乐数。
(本题和链表无关,只是作为链表设计思想的一种跨域使用)
存疑:什么是快乐数
快乐数(happy number)有以下的特性:在给定的进位制下,该数字所有数位(digits)的平方和,得到的新数再次求所有数位的平方和,如此重复进行,最终结果必定为1。
而非快乐数的特征是,进行上述规则的尝试时,最后结果会进入类似 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4的循环
思考
本题虽然不是链表,但是可以采用链表的思维,因为对于任一个数字其运算后的结果都是固定的,也就是唯一指向了一个数字。而链表的特征就是每一个节点都唯一指向某一个节点,或者指向空。
而当一个数(非快乐数)最终进入一个循环时,可以理解为,链表成环。
也就是说如果一个数字在计算的过程中,如果某一次算出的值和之前某一次的值相同则可以判定这个数字时非快乐数,而如果某一次计算出了1则可以判定是快乐数。
所以可以使用快慢指针的方式进行计算,快计算一次行动计算2次,而慢计算一次行动计算1次,当快指针的计算结果等于1或者等于慢计算时,停下。得出结论。
链表的节点删除
将链表的某些节点删除
(代码略)
解决方案
对于需要删除的节点,试图获取其前一个后其后一个,随后使得,前一个跳过需要删除的节点指向后一个。
对于需要删除的节点连续的情况,视作一个整体,试图获取这一段的前一个和后一个,随后使得,前一个跳过需要删除的节点指向后一个。
对于多段需要删除的节点的情况,每一段采用上述2个方案进行实现。
对于特殊情况,比如头部或者尾部,进行特殊的处理即可。
结语
本课于3月4日上完,
于3月7日复习完毕,作为初学课程代码层面适合进行理解。
本次学习的感觉为,耗时过久,没时间做整理,看视频学习的效率不如看讲义学习的效率。
除基本知识外还有额外思考题,如下:
彩蛋习题
求0~100000内的所有快乐数的求和(包括0和100000)。
解决方案
- 直接考虑的方案是从最小开始不断计算,将每一个快乐数进行累加。
- 如果想要减少运算域,则可以创一个boolean型数组,当一个数判断为快乐数时将这个数的索引所在置为1,或者用map进行键值对存储。这样可以用空间换时间加快整体运算的速度。
那么还有没有更好的方案呢?欢迎读者自行进行思考。
(由于课程要求,彩蛋习题不能暴露答案,因此请自行进行编码计算)