八种基础排序算法
/*
* 排序
* 包含了堆排序、快速排序、希尔排序、桶排序、归并排序、冒泡排序、选择排序、插入排序的数组实现
* 以及相应的思路和效率
* 作者:Univero
* 时间:2024-4-20
*/
#include <iostream>
#include <algorithm>
using namespace std;
// 交换函数
void swap(int *arr, int i, int j)
{
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/*
* 堆排序
* 最大堆:父节点的值大于等于其子节点的值
* 最小堆:父节点的值小于等于其子节点的值
* 思路:
* 1.将待排序的序列建构成一个最大堆(或最小堆)
* 2.将堆顶元素和最后一个元素交换,并将堆的大小减一
* 3.通过一次调整,使得堆重新满足最大堆(最小堆)性质
* 4.重复上述步骤2、3直至堆的大小为1
* 原理:
* 构造最大堆(最小堆)后,第一个元素最大,交换第一个元素和最后一个元素,保证最后一个最大
* 再将堆的大小减一,重复上述步骤确保倒数第二个最大(小)
* 效率:n*log n
* 不稳定
*/
// 调整堆,使得以root为根节点的子树满足最大堆性质(升序)
void heapify(int *num, int root, int size)
{
int largest = root; // 初始化根节点为最大值
int left = root * 2 + 1; // 计算左子节点的索引
int right = root * 2 + 2; // 计算右子节点的索引
// 找到左右节点中的最大值
// 和size的比较是因为最后一层非叶子节点可能没有子节点
if (left < size && num[left] > num[largest])
largest = left;
if (right < size && num[right] > num[largest])
largest = right;
// 如果最大值不是根节点,则交换根节点和最大值,并继续调整堆
if (largest != root)
{
swap(num, largest, root);
heapify(num, largest, size); // 交换后root处保证了最大堆的性质,但是被交换的largest节点不能保证,所以对largest递归调整
}
}
// 堆排序函数
void heap_sort(int *num, int n)
{
// 构建最大堆,从最后一个非叶子节点开始向上调整
for (int i = n / 2 - 1; i >= 0; i--)
{
heapify(num, i, n);
}
// 交换堆顶元素和最后一个元素,并调整堆,直至堆的大小为1
for (int i = n - 1; i >= 0; i--)
{
swap(num, 0, i);
heapify(num, 0, i);
}
}
/*
* 快速排序
* 思路:分治思想
* 每次将一个基准元素放入正确的位置,保证左侧元素小于等于基准元素,右侧元素大于等于基准元素
* 在对左右侧子序列进行递归操作直至序列中元素个数为1或0
* 平均:O(n^log n)
* 最坏:O(n^2)
* 不稳定
*/
// 划分函数,返回基准元素位置
// 数组中左侧都小于pivot,右侧都大于pivot
int partition(int *arr, int left, int right)
{
int pivot = arr[left];
int i = left + 1;
int j = right;
// 寻找基准元素的正确位置,同时确保左右序列的大小关系
while (true)
{
while (i <= j && arr[i] <= pivot)
i++;
while (i <= j && arr[j] >= pivot)
j--;
if (i > j) // 若i>j则说明从基准元素的下一个到都保证比基准元素小
break;
swap(arr, i, j); // 若i<=j则说明左侧子序列有元素大于基准序列需要换到右侧子序列
}
swap(arr, left, j); // 将基准元素放入正确的位置
return j; // 返回交换后的基准元素位置
}
void quick_sort(int *arr, int left, int right)
{
if (left < right)
{
// 相当于把pivot放入正确的位置后再依次对左右子列排序
int pivotIndex = partition(arr, left, right);
quick_sort(arr, left, pivotIndex - 1);
quick_sort(arr, pivotIndex + 1, right);
}
return;
}
/*
* 桶排序
* 适用于待排序数据均匀分布在一个区间范围内
* 思路:
* 将待排序的数据分到有限数量的桶中,每个桶分别排序,在按照桶的顺序依次取出
* 步骤:
* 1.确认桶的数量,可以更加数据范围、可以固定、可以动态分配
* 2.将数据分到桶中,由映射函数决定分配到哪个桶中
* 3.对每个桶进行排序,如快排
* 4.合并桶中数据
* ps:也可以开足够大的索引,直接按序取出(如果确定数据范围可以考虑)
* 关键点:如果用数组实现,注意如何分配动态的二维数组
* 自己写的快排要数组长度这一参数,用一个数组维护
*/
void bucket_sort(int *num, int n)
{
int max_val = *max_element(num, num + n); // 获取最大元素
int min_val = *min_element(num, num + n); // 获取最小元素
int bucket_size = 10; // 固定桶的大小为10
// 计算桶的数量
int bucket_count = (max_val - min_val) / bucket_size + 1;
int *bucket_num = new int[bucket_count]; // 用于统计桶中元素个数
// 创建桶
int **buckets = new int *[bucket_count];
for (int i = 0; i < bucket_count; i++)
buckets[i] = nullptr;
for (int i = 0; i < n; i++)
{
int index = (num[i] - min_val) / bucket_size;
if (buckets[index] == nullptr)
{
buckets[index] = new int[n];
bucket_num[index] = 0;
}
buckets[index][bucket_num[index]++] = num[i]; // 在桶中相应位置插入元素并更新元素数量
}
int index = 0;
for (int i = 0; i < bucket_count; i++)
{
if (buckets[i] != nullptr)
{
quick_sort(buckets[i], 0, bucket_num[i] - 1);
}
for (int j = 0; j < n && buckets[i][j]; j++)
{
num[index++] = buckets[i][j];
}
delete[] buckets[i];
}
delete[] buckets;
delete[] bucket_num;
}
/*
* 归并排序
* 思路:
* 二分序列,按顺序合并子序列实现排序
* 效率:O(n*log n)
* 稳定
*/
// 合并函数,将左右两个子序列合并
void merge(int *arr, int left, int mid, int right)
{
int len = right - left + 1;
int *tmp = new int[len]; // 分配临时数组,用于合并
int k = 0; // 临时数组的索引
int i = left, j = mid + 1; // 左右两个数组的起始位置
// 合并已经排序过的两个子序列
while (i <= mid && j <= right)
{
if (arr[i] <= arr[j])
tmp[k++] = arr[i++];
else
tmp[k++] = arr[j++];
}
// 合并剩余序列
while (i <= mid)
tmp[k++] = arr[i++];
while (j <= right)
tmp[k++] = arr[j++];
//将临时数组内容拷贝到原数组
for (k = 0; k < len; k++)
arr[k + left] = tmp[k];
// 释放临时数组内存
delete[] tmp;
}
void merge_sort(int *num, int left, int right)
{
if (left >= right) // 待排序数组长度为1或0时直接返回
return;
int mid = (left + right) / 2; // 计算中间位置
// 递归对左右子列排序
merge_sort(num, left, mid);
merge_sort(num, mid + 1, right);
// 合并排序后的结果
merge(num, left, mid, right);
}
/*
* 希尔排序
* 效率关键在于选择增量序列
* 希尔增量序列:2^k (1,2,4,6...)
* Hibbard增量序列 2^k-1 (1,3,5,7...)
* 思路:
* 由增量划分子序列,在每一个子序列中进行插入排序,直至增量为1
* 效率:O(n^1.3)~O(n^2)由增量序列选择决定
* 稳定
*/
void shell_sort(int *num, int n)
{
int increment = n / 2; // 定义一个增量(选用希尔增量,初值为n/2)
while (increment >= 1)
{
// 对每个子序列插入排序(升序)
for (int i = increment; i < n; i++)
{
int tmp = num[i];
int j = i;
while (j >= increment && num[j - increment] > tmp)
{
num[j] = num[j - increment];
j = j - increment;
}
num[j] = tmp;
}
// 缩小增量
increment /= 2;
}
}
/*
* 冒泡排序
* 思路:
* 依次交换相邻的两个元素使之有序
* O(n^2)
* 辅助空间:1
* 稳定
*/
void bubble_sort(int *num, int n)
{
// 升序
for (int i = 0; i < n; i++)
{
for (int j = n - 1; j > i; j--)
{
if (num[j] < num[j - 1])
{
swap(num, j, j - 1);
}
}
}
}
/*
* 选择排序
* 思路:
* 从未排序部分选择最值,插入已排序部分末端
* 没有好坏情况区分
* 适用于移动慢,比较快
* 辅助空间:1
* 不稳定
*/
void selection_sort(int *num, int n)
{
// 升序
for (int i = 0; i < n; i++)
{
int m = i;
// 寻找无序部分最小值
for (int j = i + 1; j < n; j++)
{
if (num[j] < num[m])
m = j; // 记录最小值索引
}
// 交换无序部分最小值至有序部分末端
if (m != i)
{
swap(num, m, i);
}
}
}
/*
* 插入排序
* 思路:
* 以第一个元素为有序表,依次往后遍历,插入有序表中的合适部分
* 注意点:移动和插入可以同时进行,索引的位置要注意(最后一个插入位置)
* 最坏:n^2
* 平均:n^2/4
* 辅助空间 1
* 稳定
*/
void insertion_sort(int *num, int n)
{
for (int i = 1; i < n; i++)
{
if (num[i] < num[i - 1])
{
int tmp = num[i];
int index = i;
// do-while循环实现(升序)
do
{
num[index] = num[index - 1];
index--;
} while (index > 0 && num[index - 1] > tmp);
num[index] = tmp;
// fro循环实现(升序)
// for (index = i - 1; index >= 0; index--)
// {
// if (num[index] > tmp)
// num[index + 1] = num[index];
// else
// break;
// }
// num[index + 1] = tmp;
}
}
}
const int MAX_NUM = 1000;
int main()
{
int num[MAX_NUM];
int n;
// 输入
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> num[i];
}
// 主要逻辑
heap_sort(num, n); // 堆排序
shell_sort(num, n); // 希尔排序
bubble_sort(num, n); // 冒泡排序
bucket_sort(num, n); // 桶排序
insertion_sort(num, n); // 插入排序
selection_sort(num, n); // 选择排序
merge_sort(num, 0, n - 1); // 归并排序
quick_sort(num, 0, n - 1); // 快速排序
// 输出
for (int i = 0; i < n; i++)
cout << num[i] << " ";
}