排序算法详解与实际应用
排序算法是计算机科学中非常重要的一类算法。它们用于将数据元素按照某种顺序排列,如从小到大或从大到小。在这篇文章中,我们将详细介绍各种常见的排序算法,并提供相应的C语言代码示例及其在实际生活中的应用。
1. 排序的基本概念
排序是指将一组无序的元素按照某种规则重新排列,使其成为有序的过程。常见的排序规则有升序和降序。排序在数据处理、搜索和优化等方面有着广泛的应用。
2. 直接插入排序
直接插入排序是一种简单直观的排序算法,它的基本思想是将数据分为已排序和未排序两部分,每次从未排序部分取一个元素插入到已排序部分的适当位置,直到全部元素有序。
算法步骤:
- 从第一个元素开始,认为它已经是有序的;
- 取下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果已经排序的元素大于新元素,则将该元素向后移动;
- 直到找到一个已排序的元素小于或等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2-5,直到所有元素有序。
C语言代码示例:
c
// 插入排序函数
void insertionSort(int arr[], int n) {
// 从第二个元素开始,逐个进行插入排序
for (int i = 1; i < n; i++) {
int key = arr[i]; // 当前待插入的元素
int j = i - 1; // 已排序部分的最后一个元素的索引
// 从已排序部分的最后一个元素开始,向前扫描
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; // 将比待插入元素大的元素向后移动
j--;
}
arr[j + 1] = key; // 将待插入元素插入到合适的位置
}
}
实际应用 :
直接插入排序适用于少量数据的排序,如扑克牌排序、学校成绩排名等。
3. 冒泡排序
冒泡排序通过重复地遍历要排序的元素列,比较相邻的两个元素并交换它们的位置来使得较大的元素逐步移动到列表的尾部。每一轮遍历都将当前未排序部分最大的元素移到未排序部分的末尾。
算法步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
C语言代码示例:
c
// 冒泡排序函数
void bubbleSort(int arr[], int n) {
// 外层循环控制比较的次数,共进行 n-1 轮
for (int i = 0; i < n - 1; i++) {
// 内层循环控制每一轮比较和交换的过程
for (int j = 0; j < n - i - 1; j++) {
// 如果前一个元素比后一个元素大,则交换它们
if (arr[j] > arr[j + 1]) {
int temp = arr[j]; // 临时变量保存较大的元素
arr[j] = arr[j + 1]; // 将较小的元素移到前面
arr[j + 1] = temp; // 将较大的元素移到后面
}
}
}
}
实际应用 :
冒泡排序适用于小规模数据排序,如初学者学习排序算法、手机联系人按名字排序等。
4. 简单选择排序
简单选择排序是一种直观的排序算法。它的基本思想是每次从未排序的部分中选择最小(或最大)的元素,将其放到已排序部分的末尾。
算法步骤:
- 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
- 重复步骤2,直到所有元素均排序完毕。
C语言代码示例:
c
// 简单选择排序函数
void selectionSort(int arr[], int n) {
// 外层循环控制选择的次数,共进行 n-1 次
for (int i = 0; i < n - 1; i++) {
int minIndex = i; // 假设当前元素是最小值
// 内层循环找到未排序部分的最小元素
for (int j = i + 1; j < n; j++) {
// 如果找到更小的元素,则更新最小元素的索引
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换当前元素和找到的最小元素
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
实际应用 :
简单选择排序常用于数据量较小且对排序性能要求不高的场合,如对少量书籍按照书名排序。
5. 希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序通过将整个待排序的记录序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
算法步骤:
- 选择一个增量序列 t1, t2, ..., tk,其中 ti > tj, tk=1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。当增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
C语言代码示例:
c
// 希尔排序函数
void shellSort(int arr[], int n) {
// 外层循环控制增量序列,从最大增量开始逐步减小
for (int gap = n / 2; gap > 0; gap /= 2) {
// 内层循环对每个子序列进行插入排序
for (int i = gap; i < n; i++) {
int temp = arr[i]; // 当前待插入的元素
int j;
// 从已排序部分的最后一个元素开始,向前扫描
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap]; // 将比待插入元素大的元素向后移动
}
arr[j] = temp; // 将待插入元素插入到合适的位置
}
}
}
实际应用 :
希尔排序适用于中小规模的数据排序,如对图书馆的书籍进行分类。
6. 快速排序
快速排序通过选择一个基准元素,并将比基准元素小的元素放在基准元素的左边,比基准元素大的元素放在基准元素的右边,然后对左右子序列继续进行快速排序,直到整个序列有序。
算法步骤:
- 选择基准元素,通常选择第一个元素或最后一个元素;
- 通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比基准元素小,另一部分记录的关键字均比基准元素大;
- 分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
C语言代码示例:
c
// 快速排序函数
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high); // 划分数组,并返回基准元素的位置
quickSort(arr, low, pi - 1); // 递归排序左子序列
quickSort(arr, pi + 1, high); // 递归排序右子序列
}
}
// 划分函数,选择基准元素,并将其余元素按大小划分
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = (low
- 1); // 较小元素的索引
for (int j = low; j < high; j++) {
// 如果当前元素小于基准元素
if (arr[j] < pivot) {
i++;
int temp = arr[i]; // 交换当前元素与较小元素
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1]; // 将基准元素放置到正确位置
arr[i + 1] = arr[high];
arr[high] = temp;
return (i + 1); // 返回基准元素的位置
}
实际应用 :
快速排序广泛应用于大规模数据排序,如数据库检索、数据分析等。
7. 堆排序
堆排序是一种树形选择排序,是对简单选择排序的有效改进。堆排序利用堆这种数据结构,堆是一种近似完全二叉树的结构,并同时满足堆积的性质。
算法步骤:
- 将待排序序列构建成一个大顶堆;
- 此时,整个序列的最大值就是堆顶的根节点;
- 将其与末尾元素进行交换,此时末尾就为最大值;
- 然后将剩余 n-1 个元素重新构建成一个大顶堆,这样会得到次大值;
- 如此反复执行,便能得到一个有序序列。
C语言代码示例:
c
// 堆排序函数
void heapSort(int arr[], int n) {
// 构建初始堆,从最后一个非叶子节点开始
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// 一次取出堆顶元素,并将其与末尾元素交换,然后调整堆
for (int i = n - 1; i > 0; i--) {
int temp = arr[0]; // 交换堆顶元素与末尾元素
arr[0] = arr[i];
arr[i] = temp;
heapify(arr, i, 0); // 调整堆,使其重新成为大顶堆
}
}
// 调整堆的函数,使以 i 为根的子树成为大顶堆
void heapify(int arr[], int n, int i) {
int largest = i; // 初始化最大值为根节点
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 如果左子节点大于根节点,则更新最大值
if (left < n && arr[left] > arr[largest])
largest = left;
// 如果右子节点大于最大值,则更新最大值
if (right < n && arr[right] > arr[largest])
largest = right;
// 如果最大值不是根节点,则交换它们,并继续调整
if (largest != i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest);
}
}
实际应用 :
堆排序适用于需要频繁插入和删除操作的大规模数据集,如优先队列、任务调度等。
8. 归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法采用分治法(Divide and Conquer)将待排序序列分成若干子序列,然后将这些子序列分别排序,最后再将排好序的子序列合并在一起。
算法步骤:
- 将待排序序列分成两部分;
- 分别对这两部分进行递归排序;
- 将排序好的两部分合并在一起。
C语言代码示例:
c
// 归并排序函数
void mergeSort(int arr[], int l, int r) {
if (l < r) {
int m = l + (r - l) / 2; // 找到中间点
mergeSort(arr, l, m); // 递归排序左半部分
mergeSort(arr, m + 1, r); // 递归排序右半部分
merge(arr, l, m, r); // 合并两部分
}
}
// 合并两个已排序的子数组
void merge(int arr[], int l, int m, int r) {
int n1 = m - l + 1; // 左半部分的长度
int n2 = r - m; // 右半部分的长度
int L[n1], R[n2]; // 临时数组
// 将数据拷贝到临时数组 L 和 R 中
for (int i = 0; i < n1; i++)
L[i] = arr[l + i];
for (int j = 0; j < n2; j++)
R[j] = arr[m + 1 + j];
int i = 0, j = 0, k = l; // 初始化索引
// 合并临时数组到原数组中
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
// 拷贝 L 数组中剩余的元素
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
// 拷贝 R 数组中剩余的元素
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
实际应用 :
归并排序适用于处理大数据量的排序,如大规模数据处理、外部排序等。
9. 基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数分配到不同的桶中,再进行逐位排序。基数排序基于键值的每位数字来分配桶。
算法步骤:
- 取得数组中的最大数,并取得位数;
- arr 为原始数组,从最低位开始,进行基数排序;
- 基数排序是按照个位数、十位数、百位数等依次进行排序。
C语言代码示例:
c
// 基数排序函数
void radixSort(int arr[], int n) {
int m = getMax(arr, n); // 获取数组中最大元素
// 按照个位、十位、百位等逐位进行计数排序
for (int exp = 1; m / exp > 0; exp *= 10)
countSort(arr, n, exp);
}
// 获取数组中的最大值
int getMax(int arr[], int n) {
int mx = arr[0]; // 初始化最大值
for (int i = 1; i < n; i++)
if (arr[i] > mx)
mx = arr[i];
return mx; // 返回最大值
}
// 计数排序函数,按照 exp 位进行排序
void countSort(int arr[], int n, int exp) {
int output[n]; // 输出数组
int i, count[10] = {0}; // 计数数组,初始化为0
// 统计每个桶中出现的次数
for (i = 0; i < n; i++)
count[(arr[i] / exp) % 10]++;
// 更新计数数组,使其表示位置
for (i = 1; i < 10; i++)
count[i] += count[i - 1];
// 反向填充输出数组
for (i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
// 将输出数组拷贝到原数组
for (i = 0; i < n; i++)
arr[i] = output[i];
}
实际应用 :
基数排序适用于排序数字和字符串,如身份证号码排序、电话号码排序等。
10. 多关键码排序
多关键码排序是对多关键字进行排序的算法,通常使用优先级排序的方法,对关键字进行多次排序,每次排序基于一个关键字,从最低优先级到最高优先级依次进行。
算法步骤:
- 确定多关键字的优先级顺序;
- 从最低优先级关键字开始排序;
- 依次对每个关键字进行排序,直到最高优先级关键字排序完成。
实际应用 :
多关键码排序适用于多维度数据的排序,如对学生成绩按照班级、学号、成绩排序。
11. 链式基数排序
链式基数
排序是基数排序的一种变体,它利用链表实现多关键码的排序,适用于处理大数据量和变长数据。
C语言代码示例:
c
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct Node {
int data; // 数据域
struct Node* next; // 指针域,指向下一个节点
} Node;
// 向链表末尾添加节点
void append(Node** head_ref, int new_data) {
Node* new_node = (Node*)malloc(sizeof(Node)); // 创建新节点
Node* last = *head_ref; // 指向链表的最后一个节点
new_node->data = new_data; // 设置新节点的数据
new_node->next = NULL; // 新节点的下一个节点为空
if (*head_ref == NULL) { // 如果链表为空
*head_ref = new_node; // 新节点作为头节点
return;
}
while (last->next != NULL) // 遍历到链表的最后一个节点
last = last->next;
last->next = new_node; // 将新节点添加到链表末尾
}
// 合并两个已排序的链表
Node* sortedMerge(Node* a, Node* b) {
Node* result = NULL;
if (a == NULL)
return b;
else if (b == NULL)
return a;
if (a->data <= b->data) { // 如果 a 的数据小于等于 b 的数据
result = a;
result->next = sortedMerge(a->next, b);
} else { // 如果 b 的数据小于 a 的数据
result = b;
result->next = sortedMerge(a, b->next);
}
return result;
}
// 链表排序函数
void mergeSort(Node** head_ref) {
Node* head = *head_ref;
Node* a;
Node* b;
if ((head == NULL) || (head->next == NULL)) { // 如果链表为空或只有一个节点
return;
}
frontBackSplit(head, &a, &b); // 将链表分为前后两部分
mergeSort(&a); // 对前半部分递归排序
mergeSort(&b); // 对后半部分递归排序
*head_ref = sortedMerge(a, b); // 合并排序后的链表
}
// 将链表分为前后两部分
void frontBackSplit(Node* source, Node** front_ref, Node** back_ref) {
Node* fast;
Node* slow;
slow = source;
fast = source->next;
while (fast != NULL) { // 快指针每次移动两步,慢指针每次移动一步
fast = fast->next;
if (fast != NULL) {
slow = slow->next;
fast = fast->next;
}
}
*front_ref = source; // 前半部分
*back_ref = slow->next; // 后半部分
slow->next = NULL; // 将前半部分与后半部分分开
}
// 打印链表
void printList(Node* node) {
while (node != NULL) {
printf("%d ", node->data); // 打印当前节点的数据
node = node->next; // 移动到下一个节点
}
}
// 主函数
int main() {
Node* res = NULL;
Node* a = NULL;
append(&a, 15); // 向链表中添加节点
append(&a, 10);
append(&a, 5);
append(&a, 20);
append(&a, 3);
append(&a, 2);
mergeSort(&a); // 对链表进行排序
printf("Sorted Linked List is: \n");
printList(a); // 打印排序后的链表
return 0;
}
实际应用 :
链式基数排序适用于链表数据的排序,如链表结构的数据库、动态增长的数据集排序等。
通过这篇文章,你应该对各种排序算法有了全面的了解,并能在实际应用中选择合适的排序算法解决问题。