冒泡排序
通过连续地比较与交换相邻元素实现排序,每次最大或最小的元素像冒泡一样被排好。
特性:
- 最差时间复杂度和平均时间复杂度为 O(n2),当输入数组完全有序时,可达到最佳时间复杂度O(n)。
- 空间复杂度为O(1),原地排序
- 稳定排序,自适应排序
void bubbleSort(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n - 1; i++) {
bool flag = false; // 标志是否发生了交换,没发生则已经有序,就提前退出
for (int j = 0; j < n - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
std::swap(nums[j], nums[j+1]);
flag = true;
}
}
if(!flag) break;
}
}
快速排序
快速排序可看成冒泡排序升级版,不是交换相邻元素,而是通过分治策略,前后交换,每次将一个元素放到正确位置。快速排序可看成二叉树的前序遍历
特性:
- 时间复杂度为O( n log n n \log_{} n nlogn),最坏情况是 O(n2)
- 空间复杂度为O(n):完全倒序的情况下,达到最差递归深度n
- 非稳定排序,自适应排序
void swap(vector<int>& nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
// 随机打乱,避免极端情况
void shuffle(vector<int>& nums) {
srand((unsigned) time(NULL));
int n = nums.size();
for (int i = 0 ; i < n; i++) {
// 生成 [i, n - 1] 的随机数
int r = i + rand()%(n-i);
swap(nums,i,r);
}
}
int partion(vector<int>& nums, int low, int high) {
// 以 nums[low] 为基准数
int pivot = nums[low];
int i = low, j = high;
while (i < j) {
while (i < j && nums[j] > pivot) j--; // 从后向前找第一个小于基准的数
while (i < j && nums[i] <= pivot) i++; // 从前往后找第一个大于基准的数
// =必须放在这里,因为i最开始等于low,所以最开始pivot=nums[i],有=才能执行i++跳过
swap(nums, i, j);
}
swap(nums, low, i); // 将基准数交换至两子数组的分界线
return i; // 返回基准数的索引
}
void quickSort(vector<int>& nums, int low, int high) {
if (low >= high) return;
int p = partion(nums, low, high);
quickSort(nums, low, p - 1);
quickSort(nums, p + 1, high);
}
void quickSort(vector<int>& nums) {
shuffle(nums);
quickSort(nums, 0, nums.size() - 1);
}
partion的第二个版本:
int partiton(vector<int>& nums,int low, int high){
int pivot=nums[low];
// 这里把 i, j 定义为开区间,同时定义:
// [lo, i) <= pivot;(j, hi] > pivot
int i=low+1, j=high;
// 当 i > j 时结束循环,以保证区间 [lo, hi] 都被覆盖
while(i<=j){
while(i<high && nums[i]<=pivot) i++; // 此 while 结束时恰好 nums[i] > pivot,从前往后找第一个比基准大的
while(j>low && nums[j]>pivot) j--; // 此 while 结束时恰好 nums[j] <= pivot,从后往前找第一个比基准小的
if(i>=j) break;
// 此时 [lo, i) <= pivot && (j, hi] > pivot
// 交换 nums[j] 和 nums[i]
swap(nums,i,j);
// 此时 [lo, i] <= pivot && [j, hi] > pivot
}
// 最后将 pivot 放到合适的位置,即 pivot 左边元素较小,右边元素较大
swap(nums,low,j);
return j;
}
插入排序
从未排序序列中选一个元素,放到已排序序列的正确位置。
特性:
- 平均时间复杂度为 O(n2),当输入数组完全有序时,可达到最佳时间复杂度O(n)。尽管插入排序的时间复杂度更高,但在数据量较小的情况下,插入排序通常更快。
- 空间复杂度为O(1),原地排序
- 稳定排序,自适应排序
void insertSort(vector<int>& nums) {
int n = nums.size();
for (int i = 1; i < n; i++) { // 默认第一个元素有序,所以从i=1开始
int preIndex = i - 1;
int cur = nums[i]; // 当前要插入到有序序列中的元素
while (preIndex >= 0 && nums[preIndex] > cur) {
nums[preIndex + 1] = nums[preIndex];
preIndex--;
}
nums[preIndex+1] = cur; // preIndex已经减了1,这里就要+1
}
}
希尔排序
希尔排序可以看成插入排序的升级版,交换不相邻的元素以对数组的局部进行排
序。先让有一定间隔之间的元素有序,再缩小间隔,相当于分成了多个逻辑组,每个组排序。
特性:
- 希尔排序时间复杂度计算非常复杂,和增量有关,时间复杂度最坏为O(n2)
- 空间复杂度为O(1)
- 非稳定排序
void shellSort(vector<int>& nums) {
int n = nums.size();
int gap = 1;
while (gap < n / 3) gap = gap * 3 + 1;
while (gap >= 1) {
for (int i = gap; i < n; i++) {
for (int j = i; j >= gap && nums[j] < nums[j - gap]; j -= gap) std::swap(nums[j], nums[j - gap]);
}
gap /= 3;
}
}
选择排序
每次从未排序区间找最大或最小的放在开头或末尾。
特性:
- 时间复杂度为 O(n2),
- 空间复杂度为O(1),原地排序
- 非稳定排序,非自适应排序
void selectionSort(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) { // 寻找未排序中最小的数
if (nums[j] < nums[minIndex]) {
minIndex = j; // 记录最小元素的索引
}
}
std::swap(nums[i], nums[minIndex]);
}
}
堆排序
堆排序可以看成选择排序的升级版,也是每次将最大或最小的元素选出来放到末尾,但借用了二叉堆的数据结构,提升了效率。
特性:
- 时间复杂度为O( n log n n \log_{} n nlogn)
- 空间复杂度为O(1),原地排序
- 非稳定排序,非自适应排序
// 堆的长度为 n ,从节点 i 开始,从顶至底堆化
void siftDown(vector<int>& nums, int n, int i) {
while (true) {
int left = i * 2 + 1;
int right = i * 2 + 2;
int maxIndex = i;
if (left<n && nums[left]>nums[maxIndex]) maxIndex = left;
if (right<n && nums[right]>nums[maxIndex]) maxIndex = right;
// 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
if(i==maxIndex) break;
std::swap(nums[maxIndex], nums[i]);
i = maxIndex;
}
}
void heapSort(vector<int>& nums) {
int n = nums.size();
// 建堆操作:堆化除叶节点以外的其他所有节点
for (int i = n / 2 - 1; i >= 0; i--) siftDown(nums, n, i);
// 从堆中提取最大元素,循环 n-1 轮
for (int i = n - 1; i > 0; i--) {
// 交换根节点与最右叶节点(交换首元素与尾元素)
std::swap(nums[0], nums[i]);
// 交换后堆化
siftDown(nums, i, 0);
}
}
归并排序
归并排序可看成二叉树的后序遍历。先划分为左右子数组,对子数组排序后再合并。
特性:
- 时间复杂度为O( n log n n \log_{} n nlogn)
- 空间复杂度为O(n)
- 稳定排序,非自适应排序
void merge(vector<int>& nums, int left, int mid, int right) {
// 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]
// 创建一个临时数组 tmp ,用于存放合并后的结果
// 可以在外面创建辅助数组,避免递归时频繁创建
vector<int> temp(right - left + 1);
int i = left, j = mid + 1;
int k = 0;
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) temp[k++] = nums[i++];
else temp[k++] = nums[j++];
}
// 将左子数组和右子数组的剩余元素复制到临时数组中
while (i <= mid) temp[k++] = nums[i++];
while (j <= right) temp[k++] = nums[j++];
// 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间
for (k = 0; k < temp.size(); k++) nums[left+k] = temp[k];
}
void mergeSort(vector<int>& nums, int left, int right) {
// 当子数组长度为 1 时终止递归
if (left >= right) return;
int mid = (left + right) / 2;
mergeSort(nums, left, mid); // 递归左子数组
mergeSort(nums, mid + 1, right); // 递归右子数组
merge(nums, left, mid, right); // 合并
}
void mergeSort(vector<int>& nums) {
mergeSort(nums, 0, nums.size() - 1);
}
计数排序
利用一个计数数组的天然有序索引排序,只适用于非负整数。
特性:
- 时间复杂度为O(m+n)
- 空间复杂度为O(m)
- 正序遍历非稳定,倒序遍历稳定
- 计数排序不是比 排序,排序的速度快于任何比较排序算法
void countSort(vector<int>& nums) {
int maxVal = nums[0];
// 统计数组最大元素
for (int num : nums) maxVal = max(maxVal, num);
vector<int> counter(maxVal + 1, 0);
// 统计各数字的出现次数
for (int num : nums) counter[num]++;
int idx = 0;
// 遍历 counter ,将各元素填入原数组 nums
for (int i = 0; i < counter.size(); i++) {
while (counter[i] > 0) {
nums[idx++] = i;
counter[i]--;
}
}
}
桶排序
桶排序可以看作计数排序的升级版。设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。
特性:
- 时间复杂度为O(n+k)
- 空间复杂度为O(n+k)
- 是否稳定取决于排序桶内元素的算法是否稳定,自适应排序
const int BUCKET_NUM = 10;
struct ListNode {
explicit ListNode(int i = 0) :mData(i), mNext(NULL) {}
ListNode* mNext;
int mData;
};
ListNode* insert(ListNode* head, int val) {
ListNode dummyNode;
ListNode* newNode = new ListNode(val);
ListNode* pre, * curr;
dummyNode.mNext = head;
pre = &dummyNode;
curr = head;
while (NULL != curr && curr->mData <= val) {
pre = curr;
curr = curr->mNext;
}
newNode->mNext = curr;
pre->mNext = newNode;
return dummyNode.mNext;
}
ListNode* Merge(ListNode* head1, ListNode* head2) {
ListNode dummyNode;
ListNode* dummy = &dummyNode;
while (NULL != head1 && NULL != head2) {
if (head1->mData <= head2->mData) {
dummy->mNext = head1;
head1 = head1->mNext;
}
else {
dummy->mNext = head2;
head2 = head2->mNext;
}
dummy = dummy->mNext;
}
if (NULL != head1) dummy->mNext = head1;
if (NULL != head2) dummy->mNext = head2;
return dummyNode.mNext;
}
void bucketSort(vector<int>& nums) {
int n = nums.size();
vector<ListNode*> buckets(BUCKET_NUM, (ListNode*)(0));
for (int i = 0; i < n; ++i) {
int index = nums[i] / BUCKET_NUM;
ListNode* head = buckets.at(index);
buckets.at(index) = insert(head, nums[i]);
}
ListNode* head = buckets.at(0);
for (int i = 1; i < BUCKET_NUM; ++i) {
head = Merge(head, buckets.at(i));
}
for (int i = 0; i < n; ++i) {
nums[i] = head->mData;
head = head->mNext;
}
}
基数排序
思想与计数排序一致,也通过统计个数来实现排序。一种多关键字的排序算法。将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串和特定格式的浮点数,所以基数排序也不是只能使用于整数。
特性:
- 时间复杂度为O(nk),最大位数为k, 一般n和k 都相对较小,时间复杂度趋向O(n)。
- 空间复杂度为O(n+d),数据为d进制
- 用的计数排序稳定则稳定,否则不一定稳定,非自适应排序。
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int maxData = data[0]; ///< 最大数
/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
/* int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;*/
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}
总结
图片来自https://www.runoob.com/w3cnote/ten-sorting-algorithm.html
参考:
https://www.hello-algo.com/chapter_sorting/sorting_algorithm/
https://www.runoob.com/w3cnote/ten-sorting-algorithm.html