1.冒泡排序(Bubble Sort)是一种基础的排序算法,其核心思想是通过重复交换相邻元素的位置,使得最大的元素逐渐“冒泡”到数组的末端。尽管它的时间复杂度较高,通常为 O(n^2),但由于其简单直观,仍然是学习排序算法时常用的一个例子。
#include <stdio.h>
// 冒泡排序函数,接收数组指针和数组大小作为参数
void bubbleSort(int *arr, int size) {
// 外层循环:控制排序的轮数,每一轮可以将最大的元素移动到数组末尾
for(int i = 0; i < size - 1; i++) {
// 使用标志位来优化排序过程,提前结束排序
_Bool flag = 1; // 默认假设没有交换,即假设数组已排序
// 内层循环:遍历数组进行相邻元素的比较和交换
for(int j = 0; j < size - 1 - i; j++) {
// 如果前一个元素大于后一个元素,则交换它们
if(arr[j] > arr[j+1]) {
flag = 0; // 表示发生了交换
// 交换 arr[j] 和 arr[j+1] 的值
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
// 如果这一轮没有发生交换,表示数组已经有序,可以提前结束排序
if(flag) break;
}
}
int main(void) {
// 初始化一个整数数组
int arr[] = {7, 64, 3, 5, 2, 35, 53, 54, 43};
// 计算数组的大小
int size = sizeof(arr) / sizeof(arr[0]);
// 调用冒泡排序函数排序数组
bubbleSort(arr, size);
// 输出排序后的数组
for(int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
return 0;
}
2.插入排序(Insertion Sort)是一种简单且直观的排序算法。它的基本思想是通过逐步插入元素到已排序的子序列中,从而实现排序。插入排序的时间复杂度为 O(n²),在元素个数较少时,插入排序通常表现得较好,且实现非常简单。
#include <stdio.h>
// 插入排序函数,接收数组和数组的大小作为参数
void insertsort(int arr[], int size) {
// 从数组的第二个元素开始,因为第一个元素默认已经是“已排序”的
for (int i = 1; i < size; i++) {
int temp = arr[i]; // 将当前元素保存在临时变量 temp 中
int j = i; // j 用于找到正确的插入位置
// 查找当前元素的插入位置
// 当 j > 0 且前一个元素大于 temp 时,说明需要将前一个元素向右移动一位
// 直到找到合适的插入位置
while (j > 0 && arr[j - 1] > temp) {
arr[j] = arr[j - 1]; // 将前一个元素移动到当前位置
j--; // 向左移动 j,继续比较
}
// 将 temp 插入到找到的位置
arr[j] = temp;
}
}
int main(void) {
// 初始化一个整数数组
int arr[] = {13, 2, 35, 4, 5, 36, 17, 855, 91, 0};
// 计算数组的大小
int size = sizeof(arr) / sizeof(arr[0]);
// 调用插入排序函数进行排序
insertsort(arr, size);
// 输出排序后的数组
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
return 0;
}
3.快速排序是一种经典的排序算法,通常基于“分治法”策略进行排序。在标准的快速排序中,通常使用一个基准元素来将数组分成两部分,从而递归地排序每一部分。而双轴快速排序(Dual-Pivot QuickSort)则是在标准快速排序的基础上进行优化,它通过使用两个基准元素来实现更高效的排序。
#include <stdio.h>
// 这是传统的快速排序实现
// void quicksort(int *arr, int start, int end) {
// if (start >= end) return; // 递归终止条件,如果区间为空或只有一个元素,则停止排序
// int left = start, right = end;
// int pivot = arr[left]; // 选择第一个元素作为基准
// while (left < right) { // 当左指针小于右指针时,继续交换
// while (arr[right] > pivot && left < right) right--; // 右边找到小于基准的元素
// arr[left] = arr[right]; // 把小于基准的元素放到左边
// while (arr[left] < pivot && left < right) left++; // 左边找到大于基准的元素
// arr[right] = arr[left]; // 把大于基准的元素放到右边
// }
// arr[left] = pivot; // 最终将基准值放到正确的位置
// quicksort(arr, start, left - 1); // 递归排序基准值左侧部分
// quicksort(arr, left + 1, end); // 递归排序基准值右侧部分
// }
void swap(int *a, int *b) {
int temp = *a; // 临时存储a的值
*a = *b; // 将b的值赋给a
*b = temp; // 将原来a的值赋给b
}
void dualPivotQuickSort(int arr[], int start, int end) {
if (start >= end) return; // 递归终止条件
// 确保两个基准元素是有序的,将较大的基准放到右侧
if (arr[start] > arr[end]) {
swap(&arr[start], &arr[end]);
}
int pivot1 = arr[start], pivot2 = arr[end]; // 取出两个基准元素
int left = start, right = end, mid = left + 1; // 使用三个指针:left, mid, right
// 对区间进行分割,直到mid指针到达right
while (mid < right) {
if (arr[mid] < pivot1) { // 如果当前元素小于第一个基准值
swap(&arr[++left], &arr[mid++]); // 将其交换到左侧
} else if (arr[mid] <= pivot2) { // 如果当前元素在两个基准之间
mid++; // 继续向前移动
} else { // 如果当前元素大于第二个基准
// 找到一个合适的位置将当前元素移到右边
while (arr[--right] > pivot2 && right > mid);
if (mid >= right) break; // 如果mid和right交错,结束排序
swap(&arr[mid], &arr[right]); // 否则交换mid和right指向的元素
}
}
// 最终将两个基准值放到合适的位置
swap(&arr[start], &arr[left]);
swap(&arr[end], &arr[right]);
// 对三个部分分别递归进行双轴快速排序
dualPivotQuickSort(arr, start, left - 1); // 排序第一个基准值左边的部分
dualPivotQuickSort(arr, left + 1, right - 1); // 排序两个基准值之间的部分
dualPivotQuickSort(arr, right + 1, end); // 排序第二个基准值右边的部分
}
int main(void) {
int arr[] = {133, 21, 13, 14, 35, 56, 37, 38, 954, 10};
int n = sizeof(arr) / sizeof(arr[0]); // 计算数组的长度
// quicksort(arr, 0, n - 1); // 传统快速排序代码被注释掉,使用双轴快速排序替代
dualPivotQuickSort(arr, 0, n - 1); // 使用双轴快速排序
// 打印排序后的数组
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
4.希尔排序(Shell Sort)是一种在插入排序的基础上进行优化的排序算法。希尔排序通过引入分组和逐步缩小步长来提高排序效率。
#include <stdio.h>
// 希尔排序函数
void shellSort(int arr[], int size){
int delta = size / 2; // 初始化步长delta为数组长度的一半
while (delta >= 1) { // 当步长大于等于1时,继续分组排序
// 对于当前的delta,我们使用插入排序对分组进行排序
for (int i = delta; i < size; ++i) { // 从第delta个元素开始进行排序
int j = i, tmp = arr[i]; // 暂存待插入的元素tmp
while (j >= delta && arr[j - delta] > tmp) { // 如果前一个分组的元素大于待插入的元素
arr[j] = arr[j - delta]; // 将前一个元素向后移动
j -= delta; // 向前跳跃delta个位置
}
arr[j] = tmp; // 将待插入的元素放到正确位置
}
delta /= 2; // 每轮完成分组插排后,将步长减半
}
}
// 主函数
int main(void) {
int arr[] = {94, 45, 83, 65, 54, 37, 55, 87, 91, 150}; // 初始化数组
int size = sizeof(arr) / sizeof(arr[0]); // 计算数组的长度
shellSort(arr, size); // 调用希尔排序进行排序
// 打印排序后的数组
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
return 0;
}
5.堆排序(Heap Sort)和归并排序(Merge Sort)。我们不仅会关注排序算法本身的实现,还会涉及堆(Heap)结构的实现与使用。
#include <stdio.h>
#include <stdlib.h>
typedef int E; // 定义类型E为int
// 小顶堆结构体定义
typedef struct MinHeap {
E *arr; // 存储堆元素的数组
int size; // 当前堆中的元素个数
int capacity; // 堆的容量
} *Heap;
// 交换两个变量的值
void swap(E *a, E *b) {
E t = *a;
*a = *b;
*b = t;
}
// 初始化堆
_Bool initHeap(Heap heap){
heap->size = 0; // 初始堆大小为0
heap->capacity = 11; // 设置堆的初始容量为11
heap->arr = malloc(sizeof(E) * heap->capacity); // 为堆的元素数组分配内存
return heap->arr != NULL; // 如果分配内存失败,返回false
}
// 向堆中插入一个新元素
_Bool insert(Heap heap, E element) {
if(heap->size == heap->capacity) return 0; // 如果堆满了,不能插入新元素
int index = ++heap->size; // 将元素插入到堆的末尾
// 堆的上浮操作,保持堆的性质
while (index > 1 && element < heap->arr[index / 2]) {
heap->arr[index] = heap->arr[index / 2]; // 如果当前元素小于父节点,则将父节点下移
index /= 2; // 更新当前元素的索引,继续向上浮动
}
heap->arr[index] = element; // 将元素放到合适的位置
return 1;
}
// 删除堆中的根元素(最小元素)
E delete(Heap heap) {
E max = heap->arr[1], e = heap->arr[heap->size--]; // 保存堆顶元素,并将堆的最后一个元素移到堆顶
int index = 1;
while (index * 2 <= heap->size) { // 只要当前结点有子结点,就继续调整
int child = index * 2;
if(child < heap->size && heap->arr[child] > heap->arr[child + 1]) // 找到左右孩子中较小的一个
child += 1;
if(e <= heap->arr[child]) break; // 如果当前元素小于等于最小孩子,则无需继续调整
else heap->arr[index] = heap->arr[child]; // 否则交换元素
index = child; // 更新当前元素的位置,继续向下调整
}
heap->arr[index] = e; // 将最后一个元素放到合适的位置
return max;
}
// 该函数实现的是对一个子树进行堆化,使得该子树满足堆的性质
void makeHeap(int* arr, int start, int end) {
while (start * 2 + 1 <= end) { // 只要有子结点,就继续调整
int child = start * 2 + 1; // 左孩子结点的索引
if(child + 1 <= end && arr[child] < arr[child + 1]) // 如果右孩子存在且右孩子较大
child++; // 更新孩子结点为右孩子
if(arr[child] > arr[start]) // 如果孩子比父节点大,交换
swap(&arr[child], &arr[start]);
start = child; // 更新当前结点为孩子结点,继续向下调整
}
}
// 堆排序实现
void heapSort(int arr[], int size) {
// 首先对所有非叶子节点进行堆化
for(int i = size / 2 - 1; i >= 0; i--)
makeHeap(arr, i, size - 1); // 从最后一个非叶子节点开始堆化
// 然后将堆顶元素(最大元素)交换到数组的末尾,再次堆化
for (int i = size - 1; i > 0; i--) {
swap(&arr[i], &arr[0]); // 交换堆顶元素与数组的最后一个元素
makeHeap(arr, 0, i - 1); // 调整堆,使堆顶保持堆的性质
}
}
// 合并两个已经排序的子数组
void merge(int arr[], int tmp[], int left, int leftEnd, int right, int rightEnd) {
int i = left, size = rightEnd - left + 1; // 保存当前合并区间的大小
while (left <= leftEnd && right <= rightEnd) { // 两个子数组都未合并完成
if(arr[left] <= arr[right]) // 如果左边的元素小于等于右边的元素
tmp[i++] = arr[left++]; // 将左边元素放入临时数组
else
tmp[i++] = arr[right++]; // 否则将右边元素放入临时数组
}
// 如果左边还有剩余元素,直接放入临时数组
while (left <= leftEnd)
tmp[i++] = arr[left++];
// 如果右边还有剩余元素,直接放入临时数组
while (right <= rightEnd)
tmp[i++] = arr[right++];
// 将临时数组中的元素拷贝回原数组
for (int j = 0; j < size; ++j, rightEnd--)
arr[rightEnd] = tmp[rightEnd];
}
// 归并排序的递归实现
void mergeSort(int arr[], int tmp[], int start, int end) {
if(start >= end) return; // 如果数组已经有序(只包含一个元素),则不进行操作
int mid = (start + end) / 2; // 计算中间位置
mergeSort(arr, tmp, start, mid); // 对左半部分进行归并排序
mergeSort(arr, tmp, mid + 1, end); // 对右半部分进行归并排序
merge(arr, tmp, start, mid, mid + 1, end); // 合并两个已经排序的部分
}
int main() {
int arr[] = {3, 5, 7, 2, 9, 0, 6, 1, 8, 4}; // 初始化数组
struct MinHeap heap; // 创建堆
initHeap(&heap); // 初始化堆
// 往堆中插入元素
// for (int i = 0; i < 10; ++i)
// insert(&heap, arr[i]);
// for (int i = 0; i < 10; ++i)
// arr[i] = delete(&heap); // 从堆中删除元素,得到有序数组
heapSort(arr, sizeof(arr) / sizeof(arr[0])); // 调用堆排序函数进行排序
for (int i = 0; i < 10; ++i) // 打印排序后的数组
printf("%d ", arr[i]);
}