本文用于刷排序算法相关题目;以及总结和分析基础排序算法。
算法的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
零、引言
底层 sort使用的排序方法
sort作为一个内置的排序方法,可以被vector等直接调用。
对于STL中的sort()算法:
- 当数据量大时,将会采用
Quick Sort
(快排),分段递归进行排序。 - 一旦分段后的数据量小于某个阈值或者待排序的序列接近有序的时候,为了避免快排的递归带来过大的额外的开销,sort()算法就自动改为
Insertion Sort(插入排序)
。 - 如果递归的层次过深,还会改用
Heap Sort(堆排序)
。
简单来说,sort并非只是普通的快速排序,除了对普通的快排进行优化,它还结合了插入排序和堆排序。
根据不同的数量级以及不同的情况,能够自动选择合适的排序算法
。
一、常用排序算法
以下是一些最基本的排序算法。虽然在C++里可以通过**std::sort()**快速排序,而且刷题时很少需要自己手写排序算法,但是熟习各种排序算法可以加深自己对算法的基本理解,以及解出由这些排序算法引申出来的题目。
- 有助于全面理解比较算法性能的方法;
- 类似的思想也能有效解决其他类型的问题;
- 排序算法常常是我们解决其它问题的第一步。
其中,快速排序、冒泡排序——属于交换排序。
快速排序
快排的实现逻辑:
- 先从数列中取出一个数作为基准数(通常取第一个数)。
- 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
- 再对左右区间重复第二步,直到各区间只有一个数。
我的完整实现:
/**
* @Description: 快速排序也属于 交换排序的一种
* @Param:
* @Return:
* @Notes: 快速排序是对 冒泡排序的一种改进。
*/
void QuickSort(vector<int> &nums, int left, int right){
//快速排序
if(left < right){
int pivot = Partition(nums, left, right);
QuickSort(nums, left, pivot-1);
QuickSort(nums, pivot+1, right);
}
}
int Partition(vector<int> &nums, int left, int right){
// 划分先将大的放在low,然后将小的放在high
// 枢轴值选择low
int pivot = nums[left];
while( left<right ){
while(left<right && nums[right] >= pivot) right--; //【注意】 这里要 >= 下面要 <= 少了!!!
nums[left] = nums[right];
while(left<right && nums[left] <= pivot) left++;
nums[right] = nums[left];
}
// 最终指针落在了 left上,为枢轴值 存放位置
nums[left] = pivot;
return left;
}
冒泡排序
我的完整代码:
/**
* @description:
* @param {*}
* @return {*}
* @notes: 这是冒大泡 —— 最经典的方法;下面还有进阶版。
*/
void bubble_sort_general(vector<int> &nums, int n){ // 高到低排好序。
int temp;
for( int i = 0; i < n-1; i++ ){
for( int j = 0; j < n-i-1 ; j++){ // 冒泡——小到大位置 找最大泡泡
if(nums[j] > nums[j+1]){
temp = nums[j+1];
nums[j+1] = nums[j];
nums[j] = temp;
}
}
}
}
/**
* @Description:
* @Param:
* @Return:
* @Notes:
*/
void bubble_sort_pro(vector<int> &nums, int n){ // 冒泡排序进阶版
bool swapped;
for( int i = 0; i < n-1; i++ ){
swapped = false;
for( int j = 0; j < n-i-1 ; j++ ){
if(nums[j] > nums[j+1]){
swap(nums[j], nums[j+1]);
swapped = true;
}
}
if(!swapped){ // 如果后续已经排好序了,就退出就好了。【加速一些了】
break;
}
}
}
其中,选择排序、堆排序 —— 属于选择排序
选择排序
/**
* @description: 找到最小的值并与其交换,从低到高位置交换
* @param {*}
* @return {*}
* @notes: 选择排序
*/
void selection_sort(vector<int> &nums, int n){ // 找到最小的值并与其交换,从低到高位置交换
int minAddr;
for(int i = 0; i < n-1;i++){
minAddr = i;
for(int j = i+1;j<n;j++){
if(nums[j] < nums[minAddr]){
minAddr = j;
}
}
swap(nums[i], nums[minAddr]);
}
}
堆排序
见二叉堆(最大堆和最小堆实现):【数据结构相关学习与设计】STL\LRU\LFU\二叉堆\堆排序\单调栈\单调队列\Union-Find等
其中,希尔排序和插入排序——属于插入排序
插入排序
适用于基本有序的排序表和数据量不大的排序表。
特点:①找出待插入位置;②一个个向后移动,为插入数值腾出对应的地址空间。 ③然后插入。
/**
* @Description: 开始插入排序
* @Param:
* @Return:
* @Notes:
*/
void insertion_sort(vector<int> &nums, int n){ // 一般的插入排序,从低到高 排好序。
for(int i = 0; i < n ;i++){
for( int j = i; j > 0 && nums[j] < nums[j-1] ;j-- ){ // 内部从后往前插入
swap(nums[j], nums[j-1]);
}
}
}
希尔排序
基于插入排序需要有序等特性,出现了 希尔排序,又称为缩小增量排序。
多次 直接插入排序。
希尔排序有点麻烦了,我理解就是为什么每次要和前面一个数比大小来确定触发调动条件,而且每次也就一步一步地走?我可不可以每次大步大步地走,一次走3个,相应的我一次也隔3个比一次。最后这个步长越来越小,直到为1个。
下面这个图很厉害,网上的侵删。
在这里插入图片描述
将数组划分成长度很小的多个小组,小组内排好序,再稍微加大小组的长度,再排一次,知道这个小组成为数组的本体
void ShellSort(int a[], int n){
int dk, i, j, temp;
//小组的个数,小组的个数从n/2个,变成n/4,再变变变,越来越少,直到变成一个
for (dk = n/2; dk >= 1; dk = dk/2) {
//因为这个小组的元素使隔了dk个,所以排的时候也要隔dk个
//有点像归并排序
for (i = dk; i < n; i++) {
if(a[i] < a[i-dk])
{
temp = a[i];
for (j = i-dk; j >= 0 && temp < a[j]; j -= dk) {
a[j+dk] = a[j];
}
a[j+dk] = temp;
}
}
}
}
以下为我的完整代码实现:
/**
* @Description: 开始直接插入排序
* @Param:
* @Return:
* @Notes:
*/
void insertion_sort(vector<int> &nums, int n){ // 一般的插入排序,从低到高 排好序。
for(int i = 0; i < n ;i++){
for( int j = i; j > 0 && nums[j] < nums[j-1] ;j-- ){ // 内部从后往前插入
swap(nums[j], nums[j-1]);
}
}
}
/**
* @Description: 希尔排序——搜小增量排序
* @Param:
* @Return:
* @Notes: 最nb的是 结合直接排序进行思考。
*/
void ShellSort(vector<int> &nums, int n){
// 相比于直接排序,shell 数组增量从dk = n/2开始;然后 主键除以二向下取整——d_i+1 = floor(d_i/2)
// 【重点】逻辑上注意:dk为数组个数==数组增量; 同时每个小数组内有n/dk 个元素。
int dk; // 对应的初始化 增量;
for( dk=n/2; dk>=1 ; dk = dk/2){//即:一直 缩小增量dk 进行排序。为了得到增序序列。
//再在每个 dk小组中进行插入排序
for(int i = dk; i<n ; i++ ){// 1、当前步长dk个数组的驱动,驱动去每个dk大数组; 2、———再进一层 for是每个小数组里面 n/dk个元素排序。
for( int j = i; (j-dk)>=0 && nums[j] < nums[j-dk] ; j-=dk){ // 注意:关键是这个地方的 `(j-dk)>=0 && ` 不能越界的基础上,还要判断是否要进行交换。
swap(nums[j], nums[j-dk]);
}
}
}
}
5260391748
5160392748
2031475869
0123456789
二、归并排序
归并排序—与上述选择、插入和交换排序思维不同。比选择、插入排序快很多。适合较多数据排序。
1、归并排序思想(拆分、合并)
1.1、拆分
相当于深搜的迭代到根部,进行拆分。
1.2、合并
【关键】左右中——深搜返回时的两个 在回溯时进行合并并排序。
1.3 合并详解
注意:上述图片表示 arr3 == result/temp (作为当前两个合并数组的临时存放中间件,里面无序有什么固定的数);
但是! arr1\arr2 分别表示 nums数组左右两边(从mid为中间 分开的)—— 这个数组需要 放置特定的数且不能打乱——所以最后要还原下 —— 在目前两个排序子数组范围内。
2、C++代码实现
下面是大佬的左闭右开:
void merge_sort(vector<int> &nums, int l,int r,vector<int> &temp){ //左闭右开
if(l+1>=r){
return ;
}
//divide
int m = l + (r-l)/2;
merge_sort(nums,l,m,temp);
merge_sort(nums,m,r,temp);
//conquer
int p = l, q = m, i = l;
while(p<m || q<r){
if(q>=r || (p < m && nums[p] <= nums[q])){
temp[i++] = nums[p++];
}else {
temp[i++] = nums[q++];
}
}
for(i = l; i < r; ++i){
nums[i] = temp[i];
}
}
下面是我的左闭右闭:
// 二路-归并排序———【关键是将以有序的左右两端直接 归并Merge()方法】【分治 —— 直接用递归】
// 左闭右闭
void mergeSortInWD(vector<int> &nums, int left, int right, vector<int> &temp){
if(left < right){
int mid = left + (right-left)/2;
mergeSortInWD(nums, left, mid, temp);
mergeSortInWD(nums, mid+1, right, temp);
// conquer 归并开始
// merge(nums, left,mid,right);
int i,j,k;
// vector<int> temp = nums;
for( i=left,j=mid+1,k=i; i<=mid && j <= right ;k++){
if(nums[i] <= nums[j]){
temp[k] = nums[i++];
}else{
temp[k] = nums[j++];
}
}
// 剩下的直接复制到nums数组中
while(i<=mid) temp[k++] = nums[i++];
while(j<=right) temp[k++] = nums[j++];
// 将nums稳定不变的;temp是交换件
// 因为return 只是中间的几个,所以原样子赋值给nums,不改变nums
for(i = j=left;j<=k;i++, j++){
nums[i] = temp[j];
}
// int k = 0; //左闭右闭
// int i = left;
// int j = mid + 1;
// while (i <= mid && j <= right) { 【上方可换为这个】
// if (nums[i] < nums[j]){
// temp[k++] = nums[i++];
// }
// else{
// temp[k++] = nums[j++];
// }
// }
// if (i == mid + 1) {
// while(j <= right)
// temp[k++] = nums[j++];
// }
// if (j == right + 1) {
// while (i <= mid)
// temp[k++] = nums[i++];
// }
// for (j = 0, i = left ; j < k; i++, j++) {
// nums[i] = temp[j];
// }
}
}
// void merge(vector<int> nums, int left, int mid, int right){
// //k 控制A中; i j 控制temp中小的放入nums中
// int i,j,k;
// vector<int> temp = nums;
// for( i=left,j=mid+1,k=i; i<=mid && j <= right ;k++){
// if(nums[i] <= nums[j]){
// temp[k] = nums[i++];
// }else{
// temp[k] = nums[j++];
// }
// }
// // 剩下的直接复制到nums数组中
// while(i<=mid) temp[k++] = nums[i++];
// while(j<=right) temp[k++] = nums[j++];
// // 将nums稳定不变的;temp是交换件
// // 因为return 只是中间的几个,所以原样子赋值给nums,不改变nums
// for(i = j=left;j<=k;i++, j++){
// nums[i] = temp[j];
// }
// }
三、快速排序应用
快速选择
快速选择一般用于求解k-thElement问题,可以在O(n)时间复杂度,O(1)空间复杂度完成求解工作。快速选择的实现和快速排序相似,不过只需要找到第k大的枢(pivot)即可,不需要对其左右再进行排序。与快速排序一样,快速选择一般需要先打乱数组,否则最坏情况下时间复杂度为O(n2),我们这里为了方便省略掉了打乱的步骤。
注意:
此处我的解决方案包含三种方法:
- 自实现快排——从大到小 180ms
- 使用
sort()
直接从大到小,使用自修改compare —— 24ms【the BEST】 - 使用101方法——实现
快速选择
两种方法:- 大佬给的快速选择 136ms
- 自己快速排序中的 partition拿出来用了。—— 52ms 【还可以】
#include <algorithm>
#include <iostream>
#include <map>
#include <unordered_map>
#include <vector>
#include <windows.h>
using namespace std;
class Solution
{
public:
bool static comp(int a, int b)
{
return a > b;
}
/**
* @Description: 方法二——注意在类内 由于需要一个全局的静态成员函数, 所以加了static;进行从大到小的比较。
* @Param:
* @Return:
* @Notes: 24ms the BEST!
*/
int findKthLargest1(vector<int> &nums, int k)
{
//从大到小排序 —— 找到 k-1 个未知的数。
int n = nums.size();
sort(nums.begin(), nums.begin() + n, comp);
return nums[k - 1];
}
/**
* @Description: 下面通过自定义快排序实现 从大到小排序。
* @Param:
* @Return:
* @Notes: 180ms
*/
int findKthLargest(vector<int> &nums, int k)
{
//从大到小排序 —— 找到 k-1 个未知的数。
QuickSort(nums, 0, nums.size() - 1);
return nums[k - 1];
}
void QuickSort(vector<int> &nums, int left, int right)
{
if (left < right)
{ //左闭右闭 从大到小排序
//相当于先序遍历, 先排好一个位置,然后继续搜索左右分别开工排序。一个个位置
int pivot = Partition(nums, left, right);
QuickSort(nums, left, pivot - 1);
QuickSort(nums, pivot + 1, right);
}
}
int Partition(vector<int> &nums, int left, int right)
{
int pivot = nums[left];
while (left < right)
{
while (left < right && nums[right] <= pivot)
right--;
nums[left] = nums[right];
while (left < right && nums[left] >= pivot)
left++;
nums[right] = nums[left];
}
nums[left] = pivot;
return left;
}
/**
* @Description: 同时改变快速排序的想法; 实现快速选择。 —— 新增
* @Param:
* @Return:
* @Notes: 注意不同于快速排序的地方——快速选择只需要找到第k大的枢轴值(pivot)即可,然后 再在大于或小于的方向缩小范围寻找 “位置”。
*/
int findKthLargest2(vector<int> &nums, int k)
{
// 仍然从小到大快速选择;找到导数第k个, 即正数第nums.size()-k+1个;但是需要数组从零开始所以是数组 nums.size()-k
int l = 0, r = nums.size() - 1, target = nums.size() - k;
while (l < r)
{
int pivot = quickSelection(nums, l, r);
if (pivot == target)
return nums[pivot];
else if (pivot > target)
r = pivot - 1;
else
{
l = pivot + 1;
}
}
// 如果说没找到l==r了;返回 此时位置即可。
return nums[l];
}
// 辅助函数 —— 快速选择位置
// 52ms
int quickSelection(vector<int> &nums, int l, int r)
{
int pivot = nums[l];
while (l < r)
{
while (l < r && nums[r] >= pivot)
r--;
nums[l] = nums[r];
while (l < r && nums[l] <= pivot)
l++;
nums[r] = nums[l];
}
// 当前l为枢轴值,定了
nums[l] = pivot;
return l;
}
// 辅助函数 —— 快速选择c++ 大佬做题
//不好136ms
int quickSelection101(vector<int> &nums, int l, int r)
{
int i = l + 1, j = r;
while (true)
{
while (i < r && nums[i] <= nums[l])
{
++i;
}
while (l < j && nums[j] >= nums[l])
{
--j;
}
if (i >= j)
{
break;
}
swap(nums[i], nums[j]);
}
swap(nums[l], nums[j]);
return j;
}
};
int main()
{
// vector<int> arr = {3,2,1,5,6,4};
vector<int> arr = {3, 1, 2, 4};
// int k = 2;
int k = 2;
Solution so;
// int rel = so.findKthLargest1(arr, k);
int rel = so.findKthLargest2(arr, k);
cout << "result is :" << rel << endl;
for (auto a : arr)
{
cout << a << endl;
}
system("pause");
return 0;
}
四、应用——桶排序
此处的关键是理解:
- map的本质:在c++中有两个关联容器,第一种是map,内部是按照key排序的,第二种是unordered_map,容器内部是无序的,使用hash组织内容的。
- map使用key 排序可以使用 第三个类方便实现;
但是如果value 排序,只能使用vector 先行容器存放map的pair<>对,进行 sort 排序。
我的代码完整实现:
#include <iostream>
#include <map>
#include <vector>
#include <windows.h>
#include <unordered_map>
#include <algorithm>
using namespace std;
class Solution {
public:
/**
* @Description: 思路:首先使用map进行频率统计; 然后 排序value大小;最后 返回前k个的 key
* @Param:
* @Return:
* @Notes:
*/
typedef pair<int,int> PAIR;
static bool cmp(const pair<int,int> a, const pair<int,int> b){
return a.second > b.second;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> sta;
// 比较仍需要转存到 vector中
//开始统计
for(auto a:nums){
sta[a]++;
}
// cout << "now the static is:";
// for(auto s:sta){
// cout<< s.first << " " << s.second << ";";
// }
// cout << endl;
vector<PAIR> vec(sta.begin(), sta.end()); // 【关键】
sort(vec.begin(), vec.end(), cmp);
vector<int> rel;
for(auto s:vec){
if(--k<0) break;
rel.push_back(s.first);
}
return rel;
}
};
int main(){
// vector<int> arr = {1,1,1,2,2,3};
vector<int> arr = {1};
// int k = 2;
int k = 1;
Solution so;
auto rel = so.topKFrequent(arr, k);
for(auto r:rel){
cout << r << endl;
}
system("pause");
return 0;
}