开始前准备一些工具函数
#pragma once
#include<iostream>
#include<vector>
using namespace std;
// 交换函数
void MySwap(int& num1, int& num2)
{
int tmp = num1;
num1 = num2;
num2 = tmp;
}
// 打印函数
void PrintArray(vector<int>& nums)
{
for (int i = 0; i < nums.size(); i++)
{
cout << nums[i] << ",";
}
cout << endl;
}
int main(void)
{
vector<int> nums;
srand((unsigned int)time(NULL));
for (int i = 0; i < 10; i++)
{
nums.push_back(rand() % 10) ;
}
cout<< "排序前:\n"<<endl;
PrintArray(nums);
// BubbleSort(nums);
// SelectSort1(nums);
// SelectSort2(nums);
// InsertSort(nums);
// ShellSort(nums);
// MergeSort(nums, 0, nums.size() - 1);
// QuickSort(nums, 0, nums.size() - 1);
// HeapSort(nums);
printf("排序后:\n");
PrintArray(nums);
return 0;
}
1. 冒泡排序
思路:
两两比较,每次确定一个最高位
i:0~nums.size() - 1,记录数组nums进行nums.size()趟排序,每次确定index = nums.size() - i -1位置的数据(该趟遍历的最大值)
j:相邻两数比较的下标j和j+1,j从0开始,j+1最大到这趟遍历该确定数据的index,j + 1 = index
优化:
如果一趟比较中没有出现交换,说明数组已经有序,可以用bool DoSwap记录单趟的比较情况
代码:
// 冒泡排序:
void BubbleSort(vector<int>& nums)
{
// i记录比较的趟数,每一趟确定index = nums.size() - i - 1位置的数据,需要nums.size()趟
for (size_t i = 0; i < nums.size(); i++)
{
// 记录单趟是否发生交换
bool DoSwap = false;
// j和j+1是比较数的下标,j+1最大为index = nums.size() - i - 1
for (size_t j = 0; j + i + 1 < nums.size(); j++)
{
if (nums[j] > nums[j + 1])
{
MySwap(nums[j], nums[j + 1]);
DoSwap = true;
}
}
// 如果单趟不产生交换,说明数组已经有序
if (!DoSwap) break;
}
}
时间复杂度:平均 O( N^2) 最差 O( N^2)
额外空间:O(1)
2. 选择排序
思路:
nums.size()趟扫描,每趟记住最小值的下标,结束扫描后交换到对应的位置
i:0~nums.size() - 1,记录数组nums进行nums.size()趟排序,每次确定i 位置的数据(该趟遍历的最小值下标)
index:记录单趟扫描的最小值下标
j:i~nums.size() - 1,每一趟扫描从i位置开始直到数组尾
代码:单趟选最小值
// 选择排序:选最小
void SelectSort1(vector<int>& nums)
{
// i记录排序趟数,第i趟排序确定i处的数据(单趟排序最小值下标)
for (size_t i = 0; i < nums.size(); i++)
{
// index记录当前趟的最小值下标
int index = i;
// j是单趟排序遍历i+1 到 nums.size() -1
for (size_t j = i + 1; j < nums.size(); j++)
{
index = nums[index] < nums[j] ? index : j;
}
// 交换位置i的数据和单趟排序最小值
MySwap(nums[i], nums[index]);
代码:单趟选最大值
// 选择排序:选最大
void SelectSort2(vector<int>& nums)
{
// i记录排序趟数,第i趟排序确定nums.size() - i - 1处的数据(单趟排序最大值下标)
for (size_t i = 0; i < nums.size(); i++)
{
// index记录当前趟的最大值下标
int index = nums.size() - i - 1;
// j是单趟排序遍历0 到 nums.size() - i - 1
for (size_t j = 0; j < nums.size() - i - 1; j++)
{
index = nums[index] > nums[j] ? index : j;
}
// 交换位置nums.size() - i - 1的数据和单趟排序最大值
MySwap(nums[nums.size() - i - 1], nums[index]);
}
}
时间复杂度:平均 O( N^2) 最差 O( N^2)
额外空间:O(1)
3. 插入排序
思路:
i:0~nums.size() - 1,记录数组第i趟排序,先记录i处的数据cur_data找插入位置insert_index
j:i - 1 ~ 0 和i处数据比较的前序数据,如果j处数据比i处数据大,数据后移,j继续往前找;如果j处数据比处数据小,j + 1 处就是要找的位置
// 插入排序
void InsertSort(vector<int>& nums)
{
// i记录排序趟数,第i趟排序抽出i处数据往前找插入位置
for (int i = 1; i < nums.size(); i++)
{
// cur_data记录当前要插入元素的值, inset_index记录要插入的位置
int cur_data = nums[i];
int insert_index = i;
// j是单趟搜索的下标,从i - 1 搜索到0
for(int j = i - 1; j >= 0; j--)
{
// j处元素比当前值大,j处元素后移,j指针继续往前找
if (nums[j] > cur_data)
{
nums[j + 1] = nums[j];
insert_index = j;
}
else // j处元素小于等于当前值,j+1处是要插入的位置
{
insert_index = j + 1;
break;
}
}
// 将cur_data填入insert_index
nums[insert_index] = cur_data;
}
}
4. 希尔排序
思路:
优化的插入排序,插入排序每次往前找1步,希尔排序每次往前找步长
希尔排序步长可以开始选择长度的1/2,然后逐步减半直到1
// 希尔排序
void ShellSort(vector<int>& nums)
{
// 定义一个步长
int len = nums.size() / 2;
while (len >= 1)
{
// 抽出i处数据往前找插入位置, i从1到nums.size() - 1
for (int i = 1; i < nums.size(); i++)
{
// cur_data记录当前要插入元素的值, inset_index记录要插入的位置
int cur_data = nums[i];
int insert_index = i;
// j是单趟搜索的下标,每次往前按步长找,直到0
for (int j = i - len; j >= 0; j -= len)
{
// j处元素比当前值大,j处元素后移步长len,j指针继续往前找
if (nums[j] > cur_data)
{
nums[j + len] = nums[j];
insert_index = j;
}
else // j处元素小于等于当前值,j + len处是要插入的位置
{
insert_index = j + len;
break;
}
}
// 将cur_data填入insert_index
nums[insert_index] = cur_data;
}
// 每趟排序完len减半
len = len / 2;
}
}
时间复杂度:平均 O( NlogN)
额外空间:O(1)
5. 归并排序
思路:
局限的归并排序:有序的两个有序序列归并成一个序列
不局限的归并排序:递归到最小相邻的两个数就是有序的序列,进行两两合并
// 局限的归并排序:
// nums的A序列[left_A, right_A]有序,B序列[left_B, right_B]有序,左闭右闭原则
// 将A和B按顺序合并得到有序数列res
// 将res按下标对应替换到nums中
void SpecialMergeSort(vector<int>& nums, int left_A, int right_A, int left_B, int right_B)
{
if (left_A < 0 || right_A >= nums.size()) return;
if (left_B < 0 || right_B >= nums.size()) return;
vector<int> res;
// 双指针i指向A序列,j指向B序列
int i = left_A;
int j = left_B;
// 比较移动ij指针,res按从小到大存储合并后的数据,直到其中一个序列走完
while (i <= right_A && j <= right_B)
{
if (nums[i] < nums[j])
{
res.push_back(nums[i]);
i++;
}
else
{
res.push_back(nums[j]);
j++;
}
}
// 把没走完的序列中的元素全部存到res中
while(i <= right_A)
{
res.push_back(nums[i]);
i++;
}
while (j <= right_B)
{
res.push_back(nums[j]);
j++;
}
// 注意这里不能直接nums = res,归并排序分割的最小单元是两个数据,nums和res的size不一样大
// 归并排序的两个子序列A和B是挨着的,所以res替换nums的[left_A, right_B]段的数据就行
// nums = res;
if (res.size() != right_B - left_A + 1) return;
for (int i = 0; i <res.size(); i++)
{
nums[left_A + i] = res[i];
}
}
// 通用的归并排序
// 递归思想:left到mid排序, mid到right排序 (左闭右闭原则)
// 将有序的两个数组用SpecialMergeSort合并
void MergeSort(vector<int>& nums, int left, int right)
{
if (nums.empty() || left < 0 || right + 1 > nums.size()) return;
if (left >= right) return;
int mid = (left + right) / 2;
// 左边数组排成有序[left, mid],左闭右闭
MergeSort(nums, left, mid);
// 右边边数组排成有序[mid + 1, right],左闭右闭
MergeSort(nums, mid + 1, right);
// 将左右两个有序数列合并
SpecialMergeSort(nums, left, mid, mid + 1, right);
/* 注意:这种分割方式会栈溢出,因为它在数组只有很少元素(如两个)时
仍然进行不必要的递归调用。这种分割没有确保当数组被分割到足够小时递归会停止,
因此递归深度可能会不必要地增加,导致栈溢出。*/
//// 左边数组排成有序[left, mid - 1],左闭右闭
//MergeSort(nums, left, mid - 1);
//// 右边边数组排成有序[mid, right],左闭右闭
//MergeSort(nums, mid , right);
//// 将左右两个有序数列合并
//SpecialMergeSort(nums, left, mid - 1, mid, right);
}
时间复杂度:平均 O( NlogN)
额外空间:O(1)
6. 快速排序
思路:
① 取首元素作为pivot,i指向pivot,j指向尾元素
② j向左移动找比pivot小的数据,交换到i位置
③ i向右移动找比pivot大的数据,交换到j位置
④ 移动到i和j相遇,相遇的位置是pivot安放的位置
⑤ 递归pivot的左侧和右侧直到有序
代码:
// 快速排序:
void QuickSort(vector<int>& nums, int left, int right)
{
// 不要写left == right,递归时小集合边界处理会栈溢出
if (left >= right) return;
int pivot = nums[left];
int i = left;
int j = right;
while (i < j)
{
// 从右向左找小于等于枢轴的元素
while (i < j && nums[j] > pivot) j--;
if (i < j) nums[i++] = nums[j]; // 交换并移动i指针
// 从左向右找大于等于枢轴的元素
while (i < j && nums[i] < pivot) i++;
if (i < j) nums[j--] = nums[i]; // 交换并移动j指针
// 注意这里不能用MySwap交换,而是将数据填到i和j指向的位置
// 注意填完数据后指针要移动
//while (j > 0 && nums[j] > pivot) j--;
//MySwap(nums[i], nums[j]);
//nums[i] = nums[j];
//while (i < right && nums[i] < pivot) i++;
//MySwap(nums[i], nums[j]);
//nums[j] = nums[i];
}
nums[i] = pivot;
QuickSort(nums, left, i - 1);
QuickSort(nums, i + 1, right);
}
7. 堆排序
思路:
分为两步,先建堆后排序
① 建堆:
从最后一个非叶子节点开始,从后往前构建大根堆
每次都把当前子树的根节点向下和左右孩子较大值进行比较和交换,直到遇到叶子节点
② 排序
把根上最大的元素依次交换到尾巴上,然后重新调整堆
建堆
void BuildHeap(vector<int>& nums, int st, int ed)
{
if (nums.empty() || st>= ed || st < 0 || ed + 1 > nums.size()) return;
// 最后一个非叶子节点
int no_leaf_index = (st + ed) / 2;
// 从最后一个非叶子节点开始,从后往前构建子树
for (int i = no_leaf_index; i >= st; --i)
{
// cur_index记录当前构建子树的根节点下标
int cur_index = i;
// 根节点没有移动到叶子节点上就一直比较,
while (cur_index <= no_leaf_index)
{
// 节点序列0开始,左孩子 = 2 * 根节点 + 1 ,右孩子 = 2 * 根节点 + 2
// 节点序列1开始,左孩子 = 2 * 根节点 ,右孩子 = 2 * 根节点 + 1
int lchild_index = 2 * cur_index + 1;
int rchild_index = 2 * cur_index + 2;
// swap_index记录左右孩子中较大值的下标,注意可能没有右孩子,一定有左孩子
int swap_index = lchild_index;
if (rchild_index <= ed) swap_index = nums[lchild_index] > nums[rchild_index] ? lchild_index : rchild_index;
// 交换的下标不能超出ed范围
if (swap_index > ed) break;
// 如果swap_index上的值比cur_index的大,交换节点,同时更新节点序号
if (nums[swap_index] > nums[cur_index])
{
MySwap(nums[cur_index], nums[swap_index]);
cur_index = swap_index;
}
else// 如果swap_index上的值比cur_index的小,跳出当前子树循环,进入下一个子树
{
break;
}
}
}
}
// 堆排序:
void HeapSort(vector<int>& nums)
{
// 1. 建堆
BuildHeap(nums, 0, nums.size() - 1);
// 2. 排序
for (int i = nums.size() - 1; i > 0; i--)
{
// 交换
MySwap(nums[0], nums[i]);
// 调整
BuildHeap(nums, 0, i - 1);
}
}
时间复杂度:平均 O( NlogN)
额外空间:O(1)