leetcode(3)排序系列题目
(1)912. 排序数组
python
快排(Quick Sort)
基本步骤为:
- 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot);
- 分割(partition):重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成;
- 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。
基于“填坑”思路的实现:
- 以(子)数组最左端元素为pivot;
- 每次先从右侧(以 right 指代)开始找到小于pivot的元素,并将其填充到左侧空出的“坑”处,再从左侧(以 left 指代)开始找到大于pivot的元素,将其填充到右侧空出的“坑”处;
- 当左右指针相遇时,将pivot放置于 left/right 处,此时得到了一个有效的分割:小于pivot的元素均在其左侧,大于pivot的元素均在其右侧(等于pivot的元素可放置于任何一边);
- 对pivot两侧的子数组递归排序,直至子数组无法再分割。
注意:随机化选择基准值pivot能够提高时间性能
归并排序 (Merge Sort)
归并排序采用经典的分治(divide-and-conquer)策略来对序列进行排序:
- 「分」的阶段首先将序列一步步分解成小的子序列进行分段排序;
- 「治」的阶段则将分段有序的子序列合并在一起,使得整个序列变得有序。
以最后一步合并分段有序的子序列为例:
设立两个指针 leftleft 和 rightright,分别指向左右两个待合并的已有序的子数组 nums[low,mid] 和 nums[mid+1,high]。 如图中所示,在合并左右两个子数组时,若 nums[right] < nums[left],则将当前更小的 nums[right]放入排序结果中。依此类推,即可得到最终排好序的数组。
堆排序(Heap Sort)
堆可看作是一个「完全二叉树」的结构
- 大根堆/大顶堆:每个节点的值均大于等于其左右孩子节点的值;
- 小根堆/小顶堆:每个节点的值均小于等于其左右孩子节点的值。
基本步骤:
- 建堆:将待排序的数组初始化为大根堆(小根堆)。此时,堆顶的元素(即根节点)即为整个数组中的最大值(最小值)。
- 交换和调整:将堆顶元素与末尾元素进行交换,此时末尾即为最大值(最小值)。除去末尾元素后,将其他 n−1 个元素重新构造成一个大根堆(小根堆),如此便可得到原数组 n个元素中的次大值(次小值)。
- 重复步骤二,直至堆中仅剩一个元素,如此便可得到一个有序序列了。
对于「升序排列」数组,需用到大根堆;
对于「降序排列」数组,则需用到小根堆。
I. 构造大根堆
从最后一个「非叶子节点」为根节点的子树出发,从右往左、从下往上进行调整操作。
对于以某个非叶子节点的子树而言,其基本的调整操作包括:
- 如果该节点大于等于其左右两个子节点,则无需再对该节点进行调整,因为它的子树已经是堆有序的;
- 如果该节点小于其左右两个子节点中的较大者,则将该节点与子节点中的较大者进行交换,并从刚刚较大者的位置出发继续进行调整操作,直至堆有序。
例:对于 nums=[5,2,1,9,6,8],其包含 n=length(nums)=6 个元素,第一个非叶子节点为 n/2−1=2,对应的基本建堆(大根堆)步骤如下:
- 第一个非叶子节点 2:nums[2]<nums[5],即节点 2 小于其左子节点 5(其右子节点不存在),需要调整交换两者。如下图所示:
- 第二个非叶子节点 1:nums[1]<nums[3] 且 nums[1]<nums[4],即节点 1 均小于其左右子节点,但其左子节点 3 更大,因此需要调整交换节点 1 与较大的子节点 3。如下图所示:
- 第三个非叶子节点 0:nums[0]<nums[1] 且 nums[0]<nums[2],即节点 0 均小于其左右子节点,但其左子节点 1 更大,因此需要调整交换节点 0 与较大的子节点 1。如下图所示:
然而,调整完节点 0 与 节点 1 后原子树的堆序已被打破,此时 nums[1]<nums[4],即节点 1 小于其右子节点 4,因此还需要继续对以节点 1 为根结点的子树继续进行调整,如下图:
至此,全部的调整完毕,建立起了一个大根堆 nums=[9,6,8,2,5,1]
II. 排序
建立起一个大根堆后,便可以对数组中的元素进行排序了。总结来看,将堆顶元素与末尾元素进行交换,此时末尾即为最大值。除去末尾元素后,将其他 n−1n−1 个元素重新构造成一个大根堆,继续将堆顶元素与末尾元素进行交换,如此便可得到原数组 nn 个元素中的次大值。如此反复进行交换、重建、交换、重建,便可得到一个「升序排列」的数组。
对于大根堆 nums=[9,6,8,2,5,1],其堆排序基本步骤如下:
- 最大元素:此时堆顶元素为最大值,将其交换到末尾,如下所示:
交换完成后,除去末尾最大元素,此时需要对堆进行重建,使得剩余元素继续满足大根堆的要求。如下所示:
- 次大元素:此时堆顶元素为待排序元素中的最大值(即原数组中的次大值),将堆顶元素交换到末尾,如下所示:
交换完成后,除去末尾最大元素,此时需要对堆进行重建,使得剩余元素继续满足大根堆的要求(省略)。 - 第三大元素:此时堆顶元素为待排序元素中的最大值(即原数组中的第三大值),将堆顶元素交换到末尾,如下所示:
交换完成后,除去末尾最大元素,此时需要对堆进行重建,使得剩余元素继续满足大根堆的要求(省略)。 - 第四大元素:此时堆顶元素为待排序元素中的最大值(即原数组中的第四大值),将堆顶元素交换到末尾,如下所示:
交换完成后,除去末尾最大元素,此时需要对堆进行重建,使得剩余元素继续满足大根堆的要求(省略)。 - 次小元素(第五大元素):此时堆顶元素为待排序元素中的最大值(即原数组中的次小元素或第五大元素),将堆顶元素交换到末尾,如下所示:
交换完成后,除去末尾最大元素,此时堆中仅剩一个元素,即为原数组中的最小值。
至此,基于大根堆的升序排列完成,
c++
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
SelectSort(nums,n);
//quickSort(nums,0,n-1,n);
return nums;
}
//插入
void InsertSort(vector<int>& nums,int n) {
for(int i=0;i<n;i++) {
int temp = nums[i];
int j = i-1;
while(j >= 0 && nums[j] >temp) {
nums[j+1] = nums[j];
j--;
}
nums[j+1] = temp;
}
}
//折半插入
void HInsertSort(vector<int>& nums,int n) {
int i,j,low,high,mid;
for( i=0;i<n;i++ ){
int tmp = nums[i];
low = 0;high = i-1;
while(low<=high) {
mid = low+(high-low)/2;
if(nums[mid] > tmp){
high = mid - 1;
}else{
low = mid + 1;
}
}
for(j=i-1;j>=high+1;j--){
nums[j+1] = nums[j];
}
nums[high+1] = tmp;
}
}
//希尔
void ShellSort(vector<int>& nums,int n){
for(int dk = n/2;dk>=1;dk=dk/2){
for(int i=dk;i<n;++i) {
if(nums[i]<nums[i-dk]){
int tmp = nums[i],j;
for(j = i-dk;j>=0&&tmp<nums[j];j-=dk){
nums[j+dk] = nums[j];
}
nums[j+dk]=tmp;
}
}
}
}
//冒泡
void BubbleSort(vector<int>& nums,int n){
for(int i=0;i<n-1;i++) {
bool flag = false;
for(int j=n-1;j>i;j--) {
if(nums[j-1]>nums[j]){
swap(nums[j-1],nums[j]);
flag = true;
}
}
if(flag == false){
return ;
}
}
}
//快排
void quickSort(vector<int>&nums ,int left,int right,int n){
if(left<right){
int pivot=patition(nums,left,right);
quickSort(nums,left,pivot-1,n);
quickSort(nums,pivot+1,right,n);
}
}
int partition(vector<int>&nums,int low,int high){
// int pivot=nums[low];//数组的第一个数为主元,会超时
int pivot=random()%(high-low+1)+low;//随机选择主元的位置,注意是主元的位置
int tmp=nums[low];
nums[low]=nums[pivot];
nums[pivot]=tmp; //交换主元和第一个元素
pivot=nums[low]; //注意这里才是主元
while(low<high){
//从后向前找比主元小的数
while(low<high && nums[high]>=pivot)--high;
nums[low]=nums[high];//把比主元小的数移到前面
//从前向后找比主元大的数
while(low<high && nums[low]<=pivot)++low;
nums[high]=nums[low];//把比主元大的数移到后面
}
nums[low]=pivot;
return low;
}
//简单选择
void SelectSort(vector<int>& nums,int n) {
for(int i=0;i<n-1;i++) {
int min = i;
for(int j=i+1;j<n;j++) {
if(nums[j]<nums[min]) min = j;
}
if(min!=i) swap(nums[i],nums[min]);
}
}
//堆排序
void adjust(vector<int> &nums, int len, int index){
int left = 2*index + 1; // index的左子节点
int right = 2*index + 2;// index的右子节点
int maxIdx = index;
if(left<len && nums[left] > nums[maxIdx]) maxIdx = left;
if(right<len && nums[right] > nums[maxIdx]) maxIdx = right;
if(maxIdx != index)
{
swap(nums[maxIdx], nums[index]);
adjust(nums, len, maxIdx);
}
}
// 堆排序
void HeapSort(vector<int> &nums, int size){
for(int i=size/2 - 1; i >= 0; i--){
adjust(nums, size, i);
}
for(int i = size - 1; i >= 1; i--){
swap(nums[0], nums[i]);
adjust(nums, i, 0);
}
}
};
(2)剑指 Offer 45. 把数组排成最小的数
1、冒泡排序
2、选择排序
3、插入排序
4、希尔排序
5、归并排序-自顶向下
6、归并排序-自底向上
7、快速排序
8、快速排序-三向切分
(3)剑指 Offer 40. 最小的k个数
2023.06.08腾讯一面
快排,按顺序返回的写法,时间复杂度是O(nlogn):
子数组长度为 1 时终止递归
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
def quickSort(arr, l, r):
if l >= r: return
i, j = l, r
while i < j:
while i < j and arr[j] >= arr[l]: j -= 1
while i < j and arr[i] <= arr[l]: i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[l], arr[i] = arr[i], arr[l]
quickSort(arr, l, i - 1)
quickSort(arr, i + 1, r)
quickSort(arr, 0, len(arr) - 1)
return arr[:k]
不用按顺序返回的写法,时间复杂度是O(n):
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k >= len(arr): return arr
def quickSort(l, r):
i, j = l, r
while i < j:
while i < j and arr[j] >= arr[l]: j -= 1
while i < j and arr[i] <= arr[l]: i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[l], arr[i] = arr[i], arr[l]
if k < i: quickSort(l, i - 1)
if k > i: quickSort(i + 1, r)
return arr[:k]
return quickSort(0, len(arr) - 1)
(4)215. 数组中的第K个最大元素
1.快排,因为要取第k大的,从大到小排列,左边是大的右边是小的,
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
n = len(nums)
left, right = 0, n - 1
while True:
idx = self.partition(nums, left, right)
if idx == k - 1:
return nums[idx]
elif idx < k - 1:
left = idx + 1
else:
right = idx - 1
def partition(self, nums, left, right):
begin = left
pivot = nums[left]
while left < right:
while left < right and nums[right] <= pivot:
right -= 1
while left < right and nums[left] >= pivot:
left += 1
if left < right:
nums[left], nums[right] = nums[right], nums[left]
nums[left], nums[begin] = nums[begin], nums[left]
return left
2.使用优先队列构造小根堆
3.直接调用大根堆
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
q = []
for n in nums:
heappush(q, -n)
while k:
res = -heappop(q)
k -= 1
return res
969. 煎饼排序
数组 arr 元素由 1 到 n(n 为数组长度)内的所有自然数组成,对数组 arr 进行排序后,数组中每个元素 i 的位置是确定的,即 i - 1。于是,我们可以从大 (n) 到小 (1),将元素 i (i = n, n - 1, ……, 1) 放置到最终的排序位置 i - 1。对每个元素 i,我们最多可由两次翻转实现:
- 当元素 i 的索引为 i - 1,即元素 i 位于最终的排序位置时,此时不做翻转,continue 进入下次循环,对元素 i - 1 进行排序。
- 当元素 i 的索引不是 i - 1 时,此时我们需要进行如下翻转:
对元素 5 进行排序,现在我们将原数组 arr = [1, 2, 5, 3, 4, 6, 7] 分成三部分看:
一部分是已排序部分 [6, 7],一部分是待排序元素及其之前的部分 [1, 2, 5],最后剩余部分为 [3, 4]
我们对比翻转前和翻转后的数组:
原数组:arr = [1, 2, 5, 3, 4, 6, 7]
翻转后:arr = [4, 3, 1, 2, 5, 6, 7]
不难发现,由原数组到翻转后数组,可由两步到达,
第一步,将 [3, 4] 翻转,得到 [4, 3],放到数组的最前面
第二步, [6, 7] 不动,将 [1, 2, 5] 整体后移,最终得到 [4, 3, 1, 2, 5, 6, 7]
class Solution:
def pancakeSort(self, arr: List[int]) -> List[int]:
n = len(arr)
res = []
for i in range(n, 0, -1):
idx = arr.index(i)
if idx == i - 1:
continue
res += [idx + 1, i]
arr = arr[idx + 1: i][::-1] + arr[0:idx + 1] + arr[i + 1:]
return res
参考资料:
写一下常见的排序算法吧!!!
912.排序数组 题目评论
各种常用排序算法的时间复杂度和空间复杂度
经典排序算法的时间复杂度和空间复杂度
『 3种排序一网打尽 』 快速排序、归并排序和堆排序详解
数据结构可视化
【宫水三叶】冒泡排序运用题