一、链表
1、链表 VS 数组
链表是和数组同等重要的底层数据结构。
- 数组适用于数据量固定,频繁查询,较少增删
- 链表适用于数据量不固定,频繁增删,较少查询
2、适配器
三个常用的顺序容器适配器: stack(栈) queue(队列) priority_queue(优先级队列)
适配器:是标准库中的一个通用概念;容器、迭代器、函数 都有适配器
本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。
一个 容器适配器 接受一种已有的容器类型,使其行为看起来像一种不同的类型。
如stack适配器接受一个顺序容器(array和forword_list除外)使其操作起来像stackstack和queue都是基于deque(双端队列)
顺序容器: vector deque list(单向链表) forward_list array string
关联容器: map set (以及是否加multi, 是否加unordered)1、什么是优先级队列呢?
其实可以理解为【披着队列外衣的堆】,注意是堆哦~
- 优先级队列的对外接口是:
- ①从队头取元素
- ②从队尾添加元素
2、为什么称之为优先级 队列呢?
因为优先级队列的内部元素是自动按照元素的权值排列的。
在缺省情况下,优先级队列(prority_queue)利用大顶堆(max_heap)完成对元素的排序,这个大顶堆色以vector数组为表现形式的完全二叉树(complete binary tree)
3、什么是堆呢?
详看链接:堆化——建堆——堆排序 https://zhuanlan.zhihu.com/p/63089552
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。
- 大顶堆 max_heap :根结点(堆头)为最大元素,即每个结点的值都不小于其左右孩子
- 小顶堆 min_heap :根结点(堆头)为最小元素,即每个结点的值都不大于其左右孩子
优先级队列的底层实现与堆的底层实现是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。如果懒得自己写堆程序,就可以用prority_queue就可以。
4、为什么要选用 优先级队列排序 ,而不选用快排呢?
因为快排需要将 map 转换为 vector 的结构。
而优先级队列则不需要转换,仅需要维护 K 个有序的序列就可以。
5、选好了使用优先级队列(也就是堆排序),那么是使用小顶堆还是大顶堆呢?
肯定会觉得 既然求前 K 个高频元素,那肯定是用大顶堆咯~ —这个答案是不对滴!
大顶堆每次弹出的都是最大的元素,而我们最后要保留的是最大的,这样做维护的代价太高,
换个思路想一下,如果用小顶堆,每次弹出去的都是最小元素,那么自然留下的就是最大的 K 个元素咯~
3、单调队列:
解决问题:
- 需要得到当前的某个范围内的最小值或最大值
队列中的元素其对应在原来的列表中的顺序必须是单调递增的。
队列中元素的大小必须是单调递 (增/减/甚至是自定义也可以)
与普通队列的区别:
- 单调队列的队首和队尾都可以出队或者入队
- 普通队列只能是队尾入队,队首出队
通常是自己通过 deque 来实现的
- 设当前准备入队的元素为e,从队尾开始把队列中的元素逐个与e对比,
- 把比e大或者与e相等的元素逐个删除,
- 直到遇到一个比e小的元素或者队列为空为止,然后把当前元素e插入到队尾。
- 其实就是在插入数据的过程中,维护队列的单调性
- 通常在做题的时候,还需要维护队列的长度,比如滑动窗口题
最后的效果相当于:
- 当插入元素比队首元素大,则将队伍中的所有元素删掉,然后再将这个元素插入
- 当插入元素比队首元素小,但是比队尾元素大,则 while 循环将从队尾开始的每个比该元素小的都删掉,最后再插入
3、单调栈
- 满足 先进后出,注意这里说的栈顶是那个唯一的出口
- 同时 从栈顶到栈底 严格递增(以单调递增为例)
- 具体实现
- 若当前进栈元素为e,从栈顶开始遍历元素,把小于e或者等于e的元素弹出栈,
- 直接遇到一个大于e的元素或者栈为空为止,然后再把e压入栈中。
4、优先队列 priority_queue
- 其实是一种堆,是一棵完全二叉树,而这个二叉树又满足一个规律——任意节点都小于(或大于)其子节点
- C++提供了内置函数 priority_queue,包含在头文件 #include 中
- 与队列的基本操作相同,top pop
5、虚拟头结点
定义在:栈区的节点 VS 堆区的节点
平时操作都是在堆区定义节点,但是栈区也可以定义,跟在堆区用起来是一样一样的
唯一的不同是,在栈区定义的节点,出了作用域之后,就会被清理掉,
而你 new 在堆区的节点,出了作用域仍然是存在的
下面两种写法都是三句话完成,new 操作返回的是地址,这块理解的还不是很透彻
- 定义在堆区:
ListNode* dummy = new ListNode( 0 ); dummy->next = head; ListNode* tail = dummy
* - 定义在栈区:
ListNode dummy(0); dummy.next = head; ListNode* tail = &dummy;
*
二、做题总结
第203题 虚拟头结点
- 「链表操作的两种方式:」
- 1、直接使用原来的链表来进行操作。
- 2、设置一个虚拟头结点在进行操作。
方法一:直接使用原来的链表进行移除节点操作
-
要分三种情况来处理:
-
1、删除的节点为头结点。注意仅用if做一次判断可是不对的哦!要用while进行循环判断
-
2、删除的节点为非头结点。循环嵌套if-else判断语句实现
-
3、第三种情况也就是输入为空链表,则直接输出初始头结点。
方法二:使用 虚拟头结点(dummyHead) 进行移除节点操作
- 因为直接在原来的链表中进行移除节点操作需要分删除节点是否为头结点的情况,因此未来解决这一问题,使用虚拟头结点,就相当于 以一种统一的逻辑来移除链表的节点。
- new一个虚拟头结点为新的头结点,将虚拟头结点指向原来的头结点也需要有一个与虚拟头结点相等的临时变量,为了便于执行接下来的删除操作
- 虚拟头结点太好用啦!这是因为单链表中,每个结点自身只能指向下一个结点。而想要删除这个结点或者在这个结点前面加一个新的结点,都需要上一个结点。
- 如果没有虚拟头结点,直接对于头结点进行操作就需要写额外的代码,因为真正的头结点没有上一个结点。有了虚拟头结点,就减少了代码量,也易于理解。
第206题 链表反转
两种解法:
-
方法一:双指针法(也就是迭代法)
-
方法二:递归法: 先确定退出条件,再修改递归参数,最后再次调用递归
双指针法:
- 这个是不需要虚拟头结点的
递归三部曲:
1、确定递归函数的参数和返回值
2、确定终止条件:cur == nullptr
3、确定单层递归逻辑
自己写的时候 参数和返回值 都没有想出来
本题的递归也要用到双指针的思路,所以递归函数的参数就是两个指针
返回值是就是链表的头结点
第142题 环形链表
思路整理:
使用快慢指针法,分别定义fast和slow指针。
第一步:判断是否有环。两个指针都是从头结点出发,fast每次移动两个节点,slow每次移动一个节点,
如果fast和slow相遇,说明指针有环。否则说明没有环
第二步:如果有环,找到环的入口。
首先要明确,fast指针一定先进入环中,如果fast和slow相遇,一定是在环中相遇,所以有三个标记的节点
- 一个是头节点
- 一个是环形入口节点
- 一个是fast与slow相遇的节点
根据公式推导出,当只循环一圈就相遇的情况下,得到 x=z 的表达式,这意味着,,从相遇节点,出发一个指针index1,从头结点出发一个指针index2,这两个指针每次只走一个节点,那么当这两个指针相遇时,就是环形入口的节点。
如果 n 大于1,其实道理是一样的,只不过fast指针在环形转n圈之后才遇到slow指针。
也就是index1指针在环里多转了(n-1)圈,然后再遇到index2,仍然是在环形入口的节点处相遇。一、注意这个判断,因为fast每次移动两个节点,因此要判断本节点及其下一个节点是否为空
二、先根据fast和slow找到,快慢指针相遇的节点
三、根据头结点和相遇节点,找到入口节点,并返回
面试题 02.07
刚开始想错了,这里是节点,可不是直线,不能与直线相交混淆了啊!!!
从某个节点开始一旦相交,那么后面的都是是相等的,这里的相交其实是可以理解为 重合 ,地址相等 + 值相等
正是因为如此,如果两个链表相交的话,尾部一定是相等的!即从中间某处开始 到尾部 是完全相等(重合)!!!
所以先计算两个链表的长度,一定是要先找到长度,如果相交的话,一定是长的链表包含了短的链表的某一段,从某个节点开始,到尾部都相等。
剑指offer52题
-
方法一:
哈希表
这个得需要遍历两次
时间复杂度和空间复杂度都比较高 -
方法二:
1、链表的长度可以直接求解 length( )2、让长的先走,走到和短的同步,两个在一起往后走
因为但凡两个链表相交,那么从交点开始到最后一定是相同的 -
方法三:
双指针法
如果A指针把链表A走完了,然后再从链表B开始走到相遇点就相当于把这两个链表的所有节点都走了一遍,同理如果B指针把链表B走完了,然后再从链表A开始一直走到相遇点也相当于把这两个链表的所有节点都走完了所以如果A指针走到链表末尾,下一步就让他从链表B开始。同理如果B指针走到链表末尾,下一步就让他从链表A开始。只要这两个链表相交最终肯定会在相交点相遇,如果不相交,最终他们都会同时走到两个链表的末尾,我们来画个图看一下。。。有点合并链表成环的感觉,