张玉环事件有感
leetcode 215. 数组中的第K个最大元素
借助本题深入了解快速排序中的随机选择,和堆的堆化、插入、删除操作
题目
215. 数组中的第K个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
1 | 输入: [3,2,1,5,6,4] 和 k = 2 |
示例 2:
1 | 输入: [3,2,3,1,2,4,5,5,6] 和 k = 4 |
说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
思路
- 暴力解法
题目要求我们找到”数组排序后的第k个最大的元素,而不是第k个不同的元素“。
因此,升序排序以后,目标元素的索引是 len - k ,这是最简单的思路。
- 最简单同时也一定是最容易编码的,编码成功的几率最高,可以用这个简单思路编码的结果和其他思路编码的结果进行比对,验证高级算法的正确性;
- 在数据规模小、对时间复杂度、空间复杂度要求不高的时候,简单问题简单做;
- 思路简单的算法考虑清楚了,有些时候能为实现高级算法铺路,这道题也是如此;
- 低级算法往往容错性最好,即在输入不满足题目条件的时候,往往还能得到正确的答案,而高级算法对输入数据的要求就非常苛刻。
复杂度分析:
- 时间复杂度:O(nlogn),这里n是数组的长度,主要是排序,语言内置的排序一般是优化的快排或者归并,O(nlogn)
- 空间复杂度:O(1)
借助 partition 操作定位到最终排定以后索引为
len - k
的那个元素(特别注意:随机化切分元素)
以下是注意事项,因为很重要,所以放在前面说:
快速排序虽然快,但是如果实现得不好,在遇到特殊测试用例的时候,时间复杂度会变得很高。
分析:我们在学习"快速排序"的时候,接触的第1个操作就是partition(切分),简单介绍如下:
partition(切分)操作,使得:
- 对于某个索引j,nums[j]已经排定,即nums[j]经过partition(切分)操作以后会放置在它“最终应该放置的地方”;
- nums[left]到nums[j - 1]中的所有元素都不大于nums[j];
- nums[j + 1]到nums[right]中的所有元素都不小于nums[j];
partition(切分)操作总能排定一个元素,还能够知道这个元素它最终所在的位置,这样每经过一次partition(切分)操作就能缩小搜索的范围,这样的思想叫做“减而治之”(是分而治之的思想的特例)。
切分过程可以不借助额外的数组空间,仅通过交换数组元素来实现。
1 | def partition(nums, l, r): |
复杂度分析:
- 时间复杂度:O(N),这里N是数组的长度,可以通过主定理证明。
- 空间复杂度:O(1)
注意:本地必须随机初始化pivot元素,否则通过时间会很慢,因为测试中有极端用例。
- 为了应对极端测试用例,使得递归树加深,可以在循环一开始的时候,随机交换第1个元素与它后面的任意1个元素的位置;
说明:最极端的是顺序数组与倒序数组,此时递归树画出来是链表,时间复杂度退化为O(N^2),根本达不到减治的效果。
1 | def partition(nums, l, r): |
- 使用双指针,将与pivot相等的元素等概率地分到pivot最终排定位置的两边。
1 | def partition(nums, l, r): |
完整快速选择实现:
1 | class Solution: |
- 优先队列、堆
优先队列的想法是很朴素的。
我们可以维护大小为K的一个小顶堆:
- 如果当前堆不满,直接添加;
- 堆满的时候,如果新读到的数小于等于堆顶,肯定不是我们要找的元素,大于堆顶,我们pop堆顶元素,插入新到的元素,让堆自己去调整内部结构。
说明:这里最合适的操作其实是replace,即直接把新读进来的数放在堆顶,然后执行下沉操作(siftDown)操作。python中有heapq.heappushpop()操作。Java中的PriorityQueue没有提供这个操作,只好先poll()再offer()。
堆的写法有很多,以下的写法大同小异,没有本质差别。
假设数组有len个元素。
思路1:把len个元素都放入一个小顶堆堆中,然后在pop()出len-k个元素,此时小顶堆只剩下k个元素,堆顶元素就是数组的第k大元素。
思路2:把len个元素都放入一个大顶堆中,然后再pop出k-1个元素,因此前k-1大的元素都被弹出了,此时大顶堆的堆顶元素就是数组中的第k个最大元素。
下面给出第2中思路的实现,这里改变了原数组,如果不能改变原数组,我们要另外使用堆的空间,可以为k规模的大小,也可以为len规模的大小。
1 | class Solution: |
考察点
这道题目考查了快速选择(快速切分)操作的特性,时间复杂度的了解,和随机化保证时间复杂度不会退化得过于严重。
堆的解法考查了堆化、插入、和删除的代码写法,是非常好的面试题目。
参考
https://leetcode-cn.com/problems/kth-largest-element-in-an-array/solution/partitionfen-er-zhi-zhi-you-xian-dui-lie-java-dai-/
钢铁是怎样炼成的
人最宝贵的是生命,生命每人只有一次。
人的一生应该这样度过:当他回忆往事的时候,他不会因为虚度年华而悔恨;也不会因为碌碌无为而羞愧,当他临死的时候,他能够说:我的整个生命和全部精力,都献给了世界上最壮丽的事业——为人类的解放而斗争。
人应当赶紧的充分的生活,因为意外的疾病和悲惨的事故随时都可能结束他的生命。
堆的实现(大顶堆、小顶堆)
1 | # 堆的实现.py |
leetcode 41.缺失的第一个正数
leetcode 41 缺失的第一个正数
问题描述
给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。
示例 1:
输入: [1,2,0] 输出: 3 示例 2:
输入: [3,4,-1,1] 输出: 2 示例 3:
输入: [7,8,9,11,12] 输出: 1
提示:
你的算法的时间复杂度应为O(n),并且只能使用常数级别的额外空间。
思路
这道题目和不使用任何额外空间的计数排序很像,因为都对时空复杂度做了严格的限制,我们都不得不改变原数组,设法将其改为我们能够利用的数据结构。否则这两道题目都无法有「真正」的解。不使用额外空间计数排序的那道题目规定数组的元素有一定的范围,我们可以利用32位整数的前16位计数,最终输出计数排序的结果。
这道题目我们要利用,关心的只有[1, N]之间的数,其他的数字我们其实并不关心,这作为前提。
算法
1 | # 如果本题没有额外的时空复杂度要求,那么就很容易实现: |
python实现简单布隆过滤器
布隆过滤器
布隆过滤器vshash table
布隆过滤器本质上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。
优点:空间效率和查询时间都远远超过一般的算法
缺点:有一定的误识别率和删除困难
对于测试元素,当它验证这个元素所对应的⼆进制位是1的时候,那么它可能存在在布隆过滤器⾥⾯, 当它验证这个元素所对应的⼆进制位只要有⼀个不为1的话,那么我们可以百分之百肯定它不在。
那么接下来要怎么判断它到底是否存在?布隆过滤器只是放在最外⾯当⼀个缓存使的,当⼀个很快速的判断使的。当B查到了之后,布隆过滤器⾥⾯是存在的,那么B会继续到这台机器的DB⾥⾯去查。C不在布隆过滤器中,就不⽤查了。
案例
- 比特币网络
- 分布式系统(Map-Reduce) - Hadoop、search engine
- 搜索引擎,经常需要把大量的网页信息,图片信息存到整个服务器里面,一般来说,不同的网页是存在不同的集群里面的。那么就先去这个集群的布隆过滤器里面查一下。
- Redis缓存
- 垃圾邮件、评论等的过滤
简单实现示例
布隆过滤器主要有构造函数,和add一个元素,search一个元素,三个API需要实现。
构造函数,我们可以假定传入二进制数组的长度(工业实现中可能会传如元素的数量,然后推断出人禁止数组的长度),需要的哈希函数数量。
add的函数签名是 add(s:str) -> None,我们假定查询的是字符串
search是search(s:str) ->bool
示例:
1 | import mmh3 |
背包问题泛型
leetcode 207.课程表
1 | # 本题可约化为: 课程安排图是否是 有向无环图(DAG)。 |
Dijkstra's Shortest Path Algorithm|Dijkstra最短路径算法
问题描述
给定一个图和图中的源顶点,找到从源到给定图中所有顶点的最短路径。
Dijkstra的算法与Prim的最小生成树算法非常相似。和Prim的MST一样,我们以给定的源为根,生成一个SPT(shortest path tree 最短路径树)。我们维护两个集合,一个集合包含最短路径树中包含的顶点,另一个集合包含尚未包含在最短路径树中的顶点。在算法的每一步,我们都会找到一个在另一个集合(尚未包含的集合)中的顶点,并且与源的距离最小。
下面是Dijkstra算法的详细步骤,用于寻找从单个源顶点到给定图中所有其他顶点的最短路径。
算法
- 创建一个集合sptSet(最短路径树集合),用来跟踪最短路径树中包含的顶点,即这个集合中的点到源的最小距离已经被计算和确定下来。开始的时候,这个集合是空的。
- 给输出图中的所有顶点分配一个距离值。初始化所有距离值为INFINITE。为源顶点分配距离值为0,这样它就会被首先选中。
- while sptSet不包含所有的顶点:
- 选取一个在sptSet中不存在的顶点u,并且它的距离值最小。
- 将u加入到sptSet中。
- 更新u的所有相邻顶点的距离值。要更新距离值,需要遍历所有相邻顶点。对于每一个相邻的顶点v,如果u的距离值(从源点)和边的权重之和小于v的距离值,那么更新v的距离值。
代码示例
1 | # Python program for Dijkstra's single |
参考
geeksForGeeks: https://www.geeksforgeeks.org/python-program-for-dijkstras-shortest-path-algorithm-greedy-algo-7/
leetcode 1519. 子树中标签相同的节点数
原题链接
https://leetcode-cn.com/problems/number-of-nodes-in-the-sub-tree-with-the-same-label/
原题简要描述
给你一棵树(即,一个连通的无环无向图),这棵树由编号从 0
到 n - 1
的 n 个节点组成,且恰好有 n - 1
条 edges
。树的根节点为节点 0
,树上的每一个节点都有一个标签,也就是字符串 labels
中的一个小写字符(编号为 i
的 节点的标签就是 labels[i]
)
边数组 edges
以 edges[i] = [ai, bi]
的形式给出,该格式表示节点 ai
和 bi
之间存在一条边。
返回一个大小为 n
的数组,其中 ans[i]
表示第 i
个节点的子树中与节点 i
标签相同的节点数。
树 T
中的子树是由 T
中的某个节点及其所有后代节点组成的树。
这道题目问题的描述有点绕,我第一遍没有读明白,后来才明白。
其实就是将树变成无环无向图,有一个顶点。所有的边都给你了,顶点用数字表示,数字对应一个字符串的索引,让你写出每个数字的子树上(包括这个数字的顶点本身),有多少个和这个数字对应索引的字符相同的。
代码
1 | # 定义一个子函数 |
1 | class Solution: |