算法细节系列(35):不一样的排序
来源:互联网 发布:科比各赛季数据统计 编辑:程序博客网 时间:2024/06/07 17:00
算法细节系列(35):不一样的排序
详细代码可以fork下Github上leetcode项目,不定期更新。
题目摘自leetcode:
- Leetcode 215. Kth Largest Element in an Array
- Leetcode 324. Wiggle Sort II
- Leetcode 179. Largest Number
- Leetcode 440. K-th Smallest in Lexicographical Order
Leetcode 215. Kth Largest Element in an Array
找第k大有点类似于找中位数,时间复杂度为何能达到
答:因为比k小的元素和比k大的元素不需要维护有序性,和中位数一个道理。
所以自然我们能够想到一种【迭代分治】的做法,用快速排序的partition思想,之所以快的原因在于找第k大元素时,可以排除一部分的查找时间。
nums = {5,1,3,2,4,9,6,7,8};找第一大的元素(9)怎么找?切分:{1,3,2,4} 5 {9,6,7,8}切分完毕后,有个很好的性质,直接排除左半部分的元素,所以我们可以用类似二分查找的结构来实现快速寻找,其实就是一个递归。
快的原因在于排除了部分不需要比较的元素,把问题归为降为原来的一半,而之所以可以这么做是因为我们只要第k大,而第k大的性质:
比它小的元素有n-k个,比它大的元素有k-1个罢了。
代码如下:
public int findKthLargest(int[] nums, int k) { k = nums.length - k; int lo = 0; int hi = nums.length - 1; while (lo < hi) { final int j = partition(nums, lo, hi); if(j < k) { lo = j + 1; } else if (j > k) { hi = j - 1; } else { break; } } return nums[k]; } private int partition(int[] a, int lo, int hi) { int i = lo; int j = hi + 1; while(true) { while(i < hi && a[++i] < a[lo]); while(j > lo && a[--j] > a[lo]); if(i >= j) { break; } exch(a, i, j); } exch(a, lo, j); return j; } private void exch(int[] a, int i, int j) { final int tmp = a[i]; a[i] = a[j]; a[j] = tmp; }
Leetcode 324. Wiggle Sort II
比较直观的做法,先排序,把最大的几个元素放在奇数位置,直到放不下。接着把剩余元素也从大到小放入偶数位置。
代码如下:
public void wiggleSort(int[] nums) { int n = nums.length; Arrays.sort(nums); int[] aux = new int[n]; for (int i = ((n - 1) % 2 == 0 ? n - 1 : n - 2), k = 0; i >= 0; i -= 2){ aux[i] = nums[k++]; } for (int i = 1, k = n - 1; i < n; i += 2){ aux[i] = nums[k--]; } for (int i = 0; i < n; i++){ nums[i] = aux[i]; } }
时间复杂度为
答案是虚拟坐标映射,映射函数如下:
private int map(int i, int n){ return (1 + 2 * i) % (n | 1);}n | 1其实是一种结合判断条件和赋值于一身的代码小技巧,实际含义:if n % 2 == 0; n = n + 1;if n % 2 != 0; n = n;map映射的效果是:n为偶数的情况:origin : 0 1 2 3 4 5 6 7 8 9 new : 1 3 5 7 9 2 4 6 8 10n为奇数的情况:origin : 0 1 2 3 4 5 6 7 8 new : 1 3 5 7 2 4 6 8 10
所以我们实际遍历的顺序是new所展示的那样,那么现在有了median这个值,就可比较median的大小来决定是否进行交换。
num与中位数相等怎么办?很有趣,因为在放入奇数坑和偶数坑是交错开来的,所以遇到中位数的情况放在原位置就好了,而不会出现两中位数在一块的情况。
代码如下:
public void wiggleSort(int[] nums) { int n = nums.length; int median = findKthLargest(nums, (nums.length + 1) / 2); int i = 0, lf = 0, rt = n - 1; while (i <= rt){ if (nums[map(i,n)] > median){ exch(nums, map(lf++, n), map(i++,n)); } else if (nums[map(i, n)] < median){ exch(nums, map(rt--, n), map(i, n)); } else{ i++; } } } private int map(int i, int n){ return (1 + 2 * i) % (n | 1); } public int findKthLargest(int[] nums, int k) { k = nums.length - k; int lo = 0; int hi = nums.length - 1; while (lo < hi) { final int j = partition(nums, lo, hi); if(j < k) { lo = j + 1; } else if (j > k) { hi = j - 1; } else { break; } } return nums[k]; } private int partition(int[] a, int lo, int hi) { int i = lo; int j = hi + 1; while(true) { while(i < hi && a[++i] < a[lo]); while(j > lo && a[--j] > a[lo]); if(i >= j) { break; } exch(a, i, j); } exch(a, lo, j); return j; } private void exch(int[] a, int i, int j) { final int tmp = a[i]; a[i] = a[j]; a[j] = tmp; }
注意:while中的终止条件怎么看?看i自增的情况和rt自减,因为它们分别属于三种不同情况,每种情况中都不可能出现同时i++和rt–,所以终止条件自然是i <= rt,而不是i < rt。
Leetcode 179. Largest Number
起初在找它们本身的性质,不难发现一些性质:
- 长度相同的number,先选number大的。
如:123,321,选择 321123,而不是123321 - 长度不同的number该如何决策?
如:830,8301,选择 8308301 还是 8301830?
一看就知道了当然是8308301咯,没错,现在有两种做法,第一种是找到这种选择的模式,编写算法,但实际情况很难做到。第二种,把判断830在8301的前面还是后面,交给计算机来完成。其实只要把两种情况列出来,判断一下大小自然就能知道谁在谁前面了。
接着神奇的事情发生了,也是我第一次接触这样的排序比较,实在高明,对于第一种情况和第二种情况是完全可以合并的,假定给了两个number:
String a1 = num1 + num2;String s2 = num2 + num1;如何决定它们的顺序?对新生成的a1和a2来一次比较,谁大就把num1放在前面,num2放在后面。
所以其实是一种特殊的排序。。。对comparator做点操作就好了。。。
代码如下:
public String largestNumber(int[] nums) { String[] s = new String[nums.length]; for (int i = 0; i < nums.length; ++i){ s[i] = nums[i] + ""; } Arrays.sort(s, new Comparator<String>() { @Override public int compare(String o1, String o2) { String s1 = o1 + o2; String s2 = o2 + o1; return s1.compareTo(s2); } }); if (s[s.length - 1].charAt(0) == '0' ) return "0"; String ans = ""; for (String n : s){ ans = n + ans; } return ans; }
所以这种排序是利用两个元素之间的关系来排的,而非像之前排序那样,每个元素是独立的。厉害厉害,不知道有什么理论可以证明这种正确性。
Leetcode 440. K-th Smallest in Lexicographical Order
字典序,有点像Trie树,如果能够联想到Trie树,可以通过preOrder步进k步,得到的结点必然是所求的结果。
两个操作:
- 访问子结点, cur = cur * 10;
- 访问相邻结点,cur = cur + 1;
看图:
何时访问子结点?何时访问邻居结点?
- 计算到相邻结点的步数,如果步数小于剩余k的能够行走的步数,访问子结点。
- 否则,访问邻居结点。
所以这个问题转换为求相邻结点所需要的步数是多少?
private int calStep(int n, long n1, long n2){ int step = 0; while (n1 <= n){ step += Math.min(n + 1, n2) - n1; n1 *= 10; n2 *= 10; } return step; }
迭代求解每一层的结点数,如第三层n2= 200, n1 = 100
,得n2 - n1 = 100
,当然这是n超过n2的情况。如果n小于n2,则说明没有那么多实际的结点,直接计算从n到该层的结点即可。
整体代码如下:
public int findKthNumber(int n, int k) { int cur = 1; k = k - 1; while (k > 0){ int steps = calStep(n, cur, cur + 1); if (steps <= k){ cur = cur + 1; k -= steps; } else{ cur *= 10; k -= 1; } } return cur; } private int calStep(int n, long n1, long n2){ int step = 0; while (n1 <= n){ step += Math.min(n + 1, n2) - n1; n1 *= 10; n2 *= 10; } return step; }
相邻结点的步数求解慢慢磨就能出来了,但这道题主要是字典序的访问子结点可以用 cur *=10 来代替,转换成了树的preOrder来求,的确高明。
- 算法细节系列(35):不一样的排序
- 算法细节系列(32):有趣的数学
- 35、不一样的C++系列--智能指针
- 算法细节系列(17):有向环检测&&拓扑排序
- 算法细节系列(1):Java swap
- 算法细节系列(12):破除想当然
- 算法细节系列(13):买卖股票
- 算法细节系列(21):贪心有理?
- 算法细节系列(23):回溯
- 算法细节系列(25):加减乘除
- 算法细节系列(26):区间
- 算法细节系列(28):线段树
- 算法细节系列(29):any sum
- 算法细节系列(30):接口设计
- 算法细节系列(31):链表
- 算法细节系列(15):Valid Parentheses系列
- 算法细节系列(20):Word Ladder系列
- (转载)不一样的冒泡排序
- Unity编辑器重写Inspector面板,面板中编辑的数据不触发场景发生变化的问题。
- PhotoView大图绘制——硬件加速限制
- Ubuntu 安装Clion和运行使用总结
- python函数一
- Linux写时复制技术(copy-on-write)
- 算法细节系列(35):不一样的排序
- python闭包函数和装饰器
- 【解决方案 二】---设置mysql5.7编码集为utf8mb4
- 使用hadoop dfsadmin -report命令查看集群运行,结果全为0,解决方法
- Spring Boot: 开发web 应用
- [机器学习入门] 李宏毅机器学习笔记-13 (Semi-supervised Learning ;半监督学习)
- Canvas之画单一色圆环
- 音乐播放器
- Sqlite的几个坑