需重点掌握的三大排序: 快速排序 + 归并排序 + 堆排序
关于排序算法的考察,需重点掌握 快速排序 、归并排序 、 堆排序 。
1. 快速排序
1.1 快速排序算法的思想与实现
快速排序的重点是要掌握其划分的思想,划分算法需要先挑选一个 元素作为 pivot。在划分执行完之后,
pivot左边的元素都小于等于它,右边的元素都大于等于它,pivot 的位置就是数组排序完成后它的最终位置。
每执行一次 划分算法,确定数组中一个元素的位置。
1 //1. 快速排序 2 void quickSort(vector<int>& nums) 3 { 4 const int len = nums.size(); 5 if(len <= 1) return; 6 std::random_shuffle(nums.begin(),nums.end());//打乱是为了避免快速排序陷入最差情况 7 quickSortHlper(nums,0,len - 1); 8 return; 9 } 10 void quickSortHlper(vector<int>& nums,int l,int r) 11 { 12 13 if(l >= r) return; 14 int mid = Partition(nums,l,r); 15 quickSortHlper(nums,l,mid - 1); 16 quickSortHlper(nums,mid+1,r); 17 return; 18 } 19 //划分 20 int Partition(vector<int>& nums,int l,int r) 21 { 22 int i = l + 1, j = r; 23 while(true) 24 { 25 while(i <= j && nums[i] <= nums[l]) i++; 26 while(i <= j && nums[j] >= nums[l]) j--; 27 // 1. 此时 [j+1,r]的元素一定都比 nums[l]大,[l+1,i-1] 内的元素一定都比 nums[l]小 28 // 2. 因为 i > j ,[l+1,j] 一定是[l+1,i-1] 的子集,[l+1,j] 内的元素一定都比 nums[l]小 29 // 3. [l+1,j] 内的元素一定都比 nums[l]小,[j+1,r]的元素一定都比 nums[l]大,所以 30 //执行 std::swap(nums[l],nums[j])后,j 就是nums[l]最终的位置。 31 if(i > j)//此时 [j+1,r]的元素一定都比 nums[l]大,[l+1,j] 内的元素一定都比 nums[l]小 32 { 33 std::swap(nums[l],nums[j]); 34 break; 35 } 36 std::swap(nums[i],nums[j]); 37 } 38 return j; 39 }
1.2 快速排序算法的应用: LeetCode 215. 数组中的第K个最大元素
1 class Solution { 2 public: 3 int findKthLargest(vector<int>& nums, int k) 4 { 5 const int N = nums.size(); 6 int k_max_index = N - k; 7 int l = 0; 8 int r = N -1; 9 while(l <= r) 10 { 11 int mid = quickSelect(nums,l,r); 12 if(mid == k_max_index) 13 { 14 return nums[mid]; 15 } 16 else if(mid < k_max_index) 17 { 18 l = mid + 1; 19 } 20 else 21 { 22 r = mid - 1; 23 } 24 } 25 return INT_MAX;//不会执行到这一步 26 } 27 28 //快速排序划分算法,以 nums[l] 为 pivot,执行完成,确定 nums[l]最终排序好的位置 29 int quickSelect(vector<int>& nums, int l,int r) 30 { 31 int i = l + 1, j = r; 32 while(true) 33 { 34 while(i <= j && nums[i] <= nums[l]) i++; 35 while(i <= j && nums[j] >= nums[l]) j--; 36 // 1. 此时 [j+1,r]的元素一定都比 nums[l]大,[l+1,i-1] 内的元素一定都比 nums[l]小 37 // 2. 因为 i > j ,[l+1,j] 一定是[l+1,i-1] 的子集,[l+1,j] 内的元素一定都比 nums[l]小 38 // 3. [l+1,j] 内的元素一定都比 nums[l]小,[j+1,r]的元素一定都比 nums[l]大,所以 39 //执行 std::swap(nums[l],nums[j])后,j 就是nums[l]最终的位置。 40 if(i > j)//此时 [j+1,r]的元素一定都比 nums[l]大,[l+1,j] 内的元素一定都比 nums[l]小 41 { 42 std::swap(nums[l],nums[j]); 43 break; 44 } 45 std::swap(nums[i],nums[j]); 46 } 47 return j; 48 } 49 };
2. 归并排序
2.1 归并排序的思想与实现
归并排序是分治思想的典型应用,使用了一个辅助数组。时间 O(N *logN) 空间 (N)
1 void mergeSort(vector<int>& nums) 2 { 3 const int len = nums.size(); 4 if(len <= 1) return; 5 vector<int> tmp(len); 6 mergeSortHelper(nums,0,len - 1,tmp); 7 return; 8 } 9 void mergeSortHelper(vector<int>& nums,int l,int r,vector<int>& tmp) 10 { 11 if(l >= r) 12 { 13 return; 14 } 15 int mid = l + (r - l)/2; 16 mergeSortHelper(nums,l,mid,tmp); 17 mergeSortHelper(nums,mid+1,r,tmp); 18 if(nums[mid] <= nums[mid+1]) 19 { 20 return; 21 } 22 int i = l, j = mid + 1,k = 0; 23 while(i <= mid && j <= r) 24 { 25 if(nums[i] <= nums[j]) 26 { 27 tmp[k++] = nums[i++]; 28 } 29 else 30 { 31 tmp[k++] = nums[j++]; 32 } 33 } 34 while(i <= mid) 35 { 36 tmp[k++] = nums[i++]; 37 } 38 while(j <= r) 39 { 40 tmp[k++] = nums[j++]; 41 } 42 for(i = l,k = 0; i <= r;++i) 43 { 44 nums[i] = tmp[k++]; 45 } 46 return; 47 }
2.2 归并排序算法的应用: LeetCode 剑指 Offer 51. 数组中的逆序对
1 class Solution { 2 public: 3 vector<int> tmp;//归并排序的辅助数组 4 5 int reversePairs(vector<int>& nums) { 6 const int len = nums.size(); 7 if(len < 2) return 0; 8 tmp.assign(len,0); 9 return merge_sort(nums,0,len - 1); 10 } 11 12 long long int merge_sort(vector<int>& nums,int l,int r) 13 { 14 if(l >= r)//归并排序递归出口 15 { 16 return 0; 17 } 18 int mid = l + (r - l)/2;//将区间一分为二 19 long long ans = merge_sort(nums,l,mid) + merge_sort(nums,mid + 1,r);//递归地求左右子数组的逆序对个数和 20 //剪枝优化: 子数组[l,r] 已经有序,横跨两个区间的逆序对个数为 0 21 if(nums[mid + 1] >= nums[mid]) 22 { 23 return ans; 24 } 25 //计算 横跨左右区间的逆序对 26 int k = 0,i = l,j = mid + 1; 27 while(i <= mid && j <= r) 28 { 29 if(nums[i] <= nums[j]) 30 { 31 tmp[k++] = nums[i++]; 32 } 33 else 34 { //执行上述递归之后,左右子数组都已经排好序 35 //此处,nums[i:mid] 都比nums[j] 大,都和num[j] 构成逆序对 36 ans += (mid - i + 1); 37 tmp[k++] = nums[j++]; 38 } 39 } 40 //将左右区间未移动到tmp的继续移到tmp 41 while(i <= mid) tmp[k++] = nums[i++]; 42 while(j <= r) tmp[k++] = nums[j++]; 43 //将排序好的元素移回原数组,放在原来的区间上,然后nums[l:r]已经排序好了 44 for(i = l,j = 0;i <= r; ) 45 { 46 nums[i++] = tmp[j++]; 47 } 48 return ans; 49 } 50 };
3. 堆排序
3.1 堆排序 是在数组上建立一个完全二叉树的堆,堆排序分为 建堆 和 排序 两个过程。
建堆,通过下沉操作建堆效率更高,具体过程是,从最后一个非叶子节点开始,然后从后往前,对每个非叶子节点 执行下沉操作。
这样之后,nums[0:N-1]上就是一个大顶堆。
排序,将堆顶元素(代表最大元素)与堆的最后一个元素交换,同时从堆范围缩小一个元素,然后新的堆顶元素进行下沉操作,
遍历执行上诉操作,则可以完成排序。
1 //3. 堆排序 2 void heapSort(vector<int>& nums) 3 { 4 int len = nums.size(); 5 for(int i = (len - 1)/2; i >= 0; --i) 6 { 7 siftDown(nums,i,len -1); 8 } 9 for(int i = len -1 ;i >= 1;) 10 { 11 swap(nums[0],nums[i]); 12 siftDown(nums,0,--i); 13 } 14 return; 15 } 16 /** 17 * @param nums 18 * @param k 当前下沉元素的下标 19 * @param end [0, end] 是堆的有效部分,[end+1,nums.size() - 1] 是nums中排好序的部分 20 */ 21 void siftDown(vector<int>& nums,int k,int end) 22 { 23 while(2 * k + 1 <= end)// k为叶节点跳出 24 { 25 int j = 2 * k + 1 ;//j是k的左子节点索引 26 if(j + 1 <= end && nums[j+1] >= nums[j]) 27 { 28 j++; 29 } 30 //nums[j]是 nums[k]值最大的子节点 31 if(nums[j] > nums[k]) 32 { 33 std::swap(nums[k],nums[j]); 34 } 35 else 36 { 37 break; 38 } 39 k = j; 40 } 41 }
3.2 堆排序的应用,c++ STL 中 的 优先队列 std::priority_queue 就是堆实现的。
LeetCode 剑指 Offer 51. 数组中的逆序对 同样可以使用堆解决。
1 int findKthLargest(vector<int>& nums, int k) 2 { 3 std::priority_queue<int,vector<int>,greater<int>> pq; 4 for(auto num : nums) 5 { 6 if(pq.size() == k && pq.top() >= num ) continue; 7 if(pq.size() == k) 8 { 9 pq.pop(); 10 } 11 pq.push(num); 12 } 13 return pq.top(); 14 }