leetcode-排序总结

leetcode-88-合并两个有序数组(merge sorted array)-java
排序两个数组
如果第一个数组后面有空余的,可以双指针,从后往前排
如果合成的那一列前面有必要的数据,后面没有,那么可以由后向前排序

leetcode-75-颜色分类(sort colors)-java
解法1(成功,1ms,很快)
用的是题目中的一次扫描个数,二次重写数组的方法,没想出来一次扫描排序
解法2(成功,0ms,极快)
如果只能扫一遍,很容易想到的就是左边存放0,右边存放2. 中间放1
设置两个index,index0记录下一个0的位置,index0左边为0,index2记录下一个2的位置,index2右边为2.
然后使用i从头到尾扫一遍,直到与index2相遇。
i遇到0就换到左边去,遇到2就换到右边去,遇到1就跳过
需要注意的是:由于index0记录第一个1的位置,因此A[index0]与A[i]交换后,A[index0]为0,A[i]为1,因此i++;
而index2记录第一个非2的位置,可能为0或1,因此A[index2]与A[i]交换后,A[index2]为2,A[i]为0或1,i不能前进,要后续判断。
由此该数组分为4段:[0,index0)–>0; [index0,i)–>1; [i,index2]–>乱序; (index2,n-1]–>2
0 0 0 1 1 1 2 1 0 2 1 2 2 2
^ ^ ^
index0 i index2

leetcode-347-前K个高频元素(top k frequent elements)-java
对map的value进行排序,
方法1:对Map的entry,按照value,进行整体的排序,如快速排序,等待。
方法2:如果要求value中前k多的,可以进行堆排序。
方法3:如果value是整数的,而且是有限的,可以进行计数排序

leetcode-56-合并区间(merge intervals)-java
两个区间若能合并,则第一个区间的右端点一定不小于第二个区间的左端点。
先将intervals进行排序,将interval的start进行排序,保证list中的start从小到大,然后建立一个list的result,先塞入第一个intervals的东西
然后将intervals从1到length-1进行循环,每次比较now和result里的最后一个prev,
如果now.start>prev.end,说明两个interval不重叠,result里加入一个和now一样的,size++,
如果不一样,说明有重合,满足合并条件,则记录新合并区间的右端点,将prev的end改为两者中最大的那个即可

leetcode-324-摆动排序 II-java
解法1(别人的)
有一个数组[a1,a2,…,an],我们怎么把它排成摇摆序列呢?
由摇摆序列的定义:nums[0] < nums[1] > nums[2] < nums[3]…,我们知道了可以分成较大一部分的数和较小一部分数,然后互相穿插即可。比如一个数组排序后为:A=[a1,a2,…,an] (a1<=a2<=…<=an),然后分成较小和较大的两部分[a1,a2,…,a(n/2)],[a(n/2+1),…,an](数组长度为奇数时不影响),再进行穿插操作。
那是不是穿插成[a1,a(n/2+1),a2,a(n/2+2),…,an]就行了呢?
其实不对,可以验证特殊情况:n比较小时且为偶数时,穿插后的序列需要满足a(n/2+1)>a2,如果a1<a2<=a(n/2+1)<an,a(n/2+1)正好是a2的后一项且与a2相等呢?即如果是[4,5,5,6]的情况呢?
那就分成了[4,5],[5,6]两部分,之后穿插成的是[4,5,5,6]并不是摇摆序列。
应该怎样排列呢?
为了方便阅读,我们在下文中定义较小的子数组为数组A,较大的子数组为数组B。显然,出现上述现象是因为nums中存在重复元素。实际上,由于穿插之后,相邻元素必来自不同子数组,所以A或B内部出现重复元素是不会出现上述现象的。所以,出现上述情况其实是因为数组A和数组B出现了相同元素,我们用r来表示这一元素。而且我们可以很容易发现,如果A和B都存在r,那么r一定是A的最大值,B的最小值,这意味着r一定出现在A的尾部,B的头部。其实,如果这一数字的个数较少,不会出现这一现象,只有当这一数字个数达到原数组元素总数的一半,才会在穿插后的出现在相邻位置。以下举几个例子进行形象地说明:
例如,对于数组[1,1,2,2,3,3],分割为[1,1,2]和[2,3,3],虽然A和B都出现了2,但穿插后为[1,2,1,3,2,3],满足要求。
而如果2的个数再多一些,即[1,1,2,2,2,3],分割为[1,1,2]和[2,2,3],最终结果为[1,2,1,2,2,3],来自A的2和来自B的2出现在了相邻位置。
出现这一问题是因为重复数在A和B中的位置决定的,因为r在A尾部,B头部,所以如果r个数太多(大于等于(length(nums) + 1)/2),就可能在穿插后相邻。要解决这一问题,我们需要使A的r和B的r在穿插后尽可能分开。一种可行的办法是将A和B反序:
例如,对于数组[1,1,2,2,2,3],分割为[1,1,2]和[2,2,3],分别反序后得到[2, 1, 1]和[3, 2, 2],此时2在A头部,B尾部,穿插后就不会发生相邻了。
当然,这只能解决r的个数等于(length(nums) + 1)/2的情况,如果r的个数大于(length(nums) + 1)/2,还是会出现相邻。但实际上,这种情况是不存在有效解的,也就是说,这种数组对于本题来说是非法的。
此时我们得到了第一个解法,由于需要使用排序,所以时间复杂度为O(NlogN),由于需要存储A和B,所以空间复杂度为O(N)。

解法2:快速选择 + 3-way-partition
上一解法之所以时间复杂度为O(NlogN),是因为使用了排序。但回顾解法1,我们发现,我们实际上并不关心A和B内部的元素顺序,只需要满足A和B长度相同(或相差1),且A中的元素小于等于B中的元素,且r出现在A的头部和B的尾部即可。实际上,由于A和B长度相同(或相差1),所以r实际上是原数组的中位数,下文改用mid来表示。因此,我们第一步其实不需要进行排序,而只需要找到中位数即可。而寻找中位数可以用快速选择算法实现,时间复杂度为O(n)。
该算法与快速排序算法类似,在一次递归调用中,首先进行partition过程,即利用一个元素将原数组划分为两个子数组,然后将这一元素放在两个数组之间。两者区别在于快速排序接下来需要对左右两个子数组进行递归,而快速选择只需要对一侧子数组进行递归,所以快速选择的时间复杂度为O(n)。详细原理可以参考有关资料,此处不做赘述。
在C++中,可以用STL的nth_element()函数进行快速选择,这一函数的效果是将数组中第n小的元素放在数组的第n个位置,同时保证其左侧元素不大于自身,右侧元素不小于自身。
找到中位数后,我们需要利用3-way-partition算法将中位数放在数组中部,同时将小于中位数的数放在左侧,大于中位数的数放在右侧。该算法与快速排序的partition过程也很类似,只需要在快速排序的partition过程的基础上,添加一个指针k用于定位大数:
在这一过程中,指针j和k从左右两侧同时出发相向而行,每次要么j移动一步,要么k移动一步,直到相遇为止。这一过程的时间复杂度显然为O(N)。
至此,原数组被分为3个部分,左侧为小于中位数的数,中间为中位数,右侧为大于中位数的数。之后的做法就与解法1相同了:我们只需要将数组从中间等分为2个部分,然后反序,穿插,即可得到最终结果。

解法3:快速选择 + 3-way-partition + 虚地址
接下来,我们思考如何简化空间复杂度。上文提到,解法1和2之所以空间复杂度为O(N),是因为最后一步穿插之前,需要保存A和B。在这里我们使用所谓的虚地址的方法来省略穿插的步骤,或者说将穿插融入之前的步骤,即在3-way-partiton(或排序)的过程中顺便完成穿插,由此来省略保存A和B的步骤。“地址”是一种抽象的概念,在本题中地址就是数组的索引。
BTW,由于虚地址较为抽象,需要读者有一定的数学基础和抽象思维能力,如果实在理解不了没有关系,解法2已经是足够优秀的解法。
如果读者学习过操作系统,可以利用操作系统中的物理地址空间和逻辑地址空间的概念来理解。简单来说,这一方法就是将数组从原本的空间映射到一个虚拟的空间,虚拟空间中的索引和真实空间的索引存在某种映射关系。在本题中,我们需要建立一种映射关系来描述“分割”和“穿插”的过程,建立这一映射关系后,我们可以利用虚拟地址访问元素,在虚拟空间中对数组进行3-way-partition或排序,使数组在虚拟空间中满足某一空间关系。完成后,数组在真实空间中的空间结构就是我们最终需要的空间结构。
在某些场景下,可能映射关系很简洁,有些场景下,映射关系可能很复杂。而如果映射关系太复杂,编程时将会及其繁琐容易出错。在本题中,想建立一个简洁的映射,有必要对前面的3-way-partition进行一定的修改,我们不再将小数排在左边,大数排在右边,而是将大数排在左边,小数排在右边,在这种情况下我们可以用一个非常简洁的公式来描述映射关系:
#define A(i) nums[(1+2*(i)) % (n|1)],i是虚拟地址,(1+2*(i)) % (n|1)是实际地址。其中n为数组长度,‘|’为按位或,如果n为偶数,(n|1)为n+1,如果n为奇数,(n|1)仍为n。

剑指offer-30-最小的k个数-java
这道题最简单的思路莫过于把输入的n个整数排序,排序之后位于最前面的k个数就是最小的k个数。这种思路的时间复杂度是O(nlogn)。
解法一:O(n)的算法,只有当我们可以修改输入的数组时可用
我们同样可以基于Partition函数来解决这个问题。如果基于数组的第k个数字来调整,使得比第k个数字小的所有数字都位于数组的左边,比第k个数字大的所有数字都位于数组的右边。这样调整之后,位于数组中左边的k个数字就是最小的k个数字。

解法二:O(nlogk)的算法,特别适用处理海量数据
我们可以先创建一个大小为k的数据容器来存储最小的k个数字,接下来我们每次从输入的n个整数中读入一个数。如果容器中已有数字少于k个,则直接把这次读入的整数放入容器中;如果容器中已有k个数字了,也就是容器已满,此时我们不能再插入新的数字了而只能替换已有的数字。找出这已有的k个数中的最大值,然后拿这次待插入的整数和最大值进行比较。如果待插入的值比当前已有的最小值小,则用这个数替换当前已有的最大值;如果待插入的值比当前已有的最大值还大,那么这个数不可能是最小的k个整数之一,于是我们可以抛弃这个整数。

因此当容器满了之后,我们要做3件事;一是在k个整数中找到最大数;二是有可能在这个容器中删除最大数;三是有可能要插入一个新的数字。如果用一个二叉树来实现这个容器,那么我们能在O(logk)时间内实现这三步操作。因此对于n个输入的数字而言,总的时间效率是O(nlogk).
我们可以选择用不同的二叉树来实现这个数据容器。由于每次都需要找到k个整数中的最大数字,我们很容易想到用最大堆。在最大堆中,根节点的值总是大于它的子树中的任意结点的值。于是我们每次可以在O(1)得到已有的k个数字中的最大值,但需要O(logk)时间完成删除及插入操作。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值