10.排序
按存储介质可分为:
- 内部排序:数据量不大、数据在内存,无需内外存交换数据
- 外部数据:数据量较大、数据在外存(文件排序)
外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多
按比较器个数可分为:
- 串行排序:单处理机(同一时刻比较一对元素)
- 并行排序:多处理机(同一时刻比较多对元素)
按主要操作可分为:
- 比较排序:用比较的方法(插入排序、交换排序、选择排序、归并排序)
- 基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置。
按辅助空间:
- 原地排序:辅助空间用量O(1)的排序方法。(所占辅助存储空间与参加排序的数据量大小无关)
- 非原地排序:辅助空间用超过O(1)的排序方法。
按稳定性可分为:(排序方法是否稳定,并不能衡量一个排序算法的优劣)
- 稳定排序:能够使任何数值相等的元素,排序后相对次序不变。
- 非稳定性排序:不是稳定排序的方法。
按自然性可分为:
- 自然排序:输入数据越有序,排序的速度越快的排序算法。
- 非自然排序:不是自然排序的方法。
10.1 插入排序
基本思想:
每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
即边插入边排序,保证子序列中随时都是排好序的。
1.直接插入排序
使用哨兵优化:
代码实现如上:
直接插入排序是一种简单直观的排序算法。它的基本思想是将待排序的元素依次插入到已排序序列中的适当位置,从而形成新的有序序列。
具体实现步骤如下:
- 假设待排序序列为arr,初始时将arr[0]看作已排序序列。
- 从arr[1]开始,依次将arr[i]插入到已排序序列中正确的位置,使得插入后的序列仍然有序。
- 重复步骤2,直到整个序列有序。
代码示例(使用C++语言实现):
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
// 将比key大的元素依次向后移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
// 插入key到正确位置
arr[j + 1] = key;
}
}
时间复杂度为O(n^2),空间复杂度为O(1)。对于小规模的数据集,直接插入排序是一个简单且有效的排序算法。
2.折半插入排序
查找插入位置时采用折半查找法。
#include <iostream>
using namespace std;
void binaryInsertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int left = 0;
int right = i - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] > key) {
right = mid - 1;
} else {
left = mid + 1;
}
}
for (int j = i - 1; j >= left; j--) {
arr[j + 1] = arr[j];
}
arr[left] = key;
}
}
int main() {
int arr[] = {37, 23, 0, 17, 12, 72, 31, 46, 100, 88, 54};
int n = sizeof(arr) / sizeof(arr[0]);
binaryInsertionSort(arr, n);
cout << "Sorted array is: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
3.希尔排序
特点:
- 一次移动,移动位置较大,跳跃式地接近排序后的最终位置
- 最后一次只需少量移动
- 增量序列必须时递减的,最后一个必须是1
- 增量序列应该是互质的
#include <iostream>
using namespace std;
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;
}
}
}
int main() {
int arr[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
int n = sizeof(arr) / sizeof(arr[0]);
shellSort(arr, n);
cout << "Sorted array is: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
10.2 交换排序
常见交换排序方法:冒泡排序O(n^2) 快速排序O(nlogn)
1.冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法,其基本思想是将待排序的数组按照一定的顺序进行两两比较,如果前一个元素大于后一个元素,则交换它们的位置。经过一轮比较后,最大的元素会被移动到数组的末尾。然后再对除最大元素外的其他元素进行相同的操作,直到整个数组有序。
以下是冒泡排序的 C++ 代码实现:
#include <iostream>
using namespace std;
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
}
}
}
}
int main() {
int arr[] = {37, 23, 0, 17, 12, 72, 31, 46, 100, 88, 54};
int n = sizeof(arr) / sizeof(arr[0]);
bubbleSort(arr, n);
cout << "Sorted array is: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
这段代码首先定义了一个名为 bubbleSort
的函数,该函数接受一个整数数组和数组的长度作为参数。在 main
函数中,我们创建了一个待排序的整数数组,并调用 bubbleSort
对数组进行排序。最后,我们输出排序后的数组。
优化上述核心代码:当后序排列时它们位置已经相对正确,可不进行比较直接结束。
void bubbleSort(int arr[], int n) {
bool flag = true;//flag作为是否有交换的标记
for (int i = 0; i < n - 1 && flag == true; i++) {
flag = false;//flag用来标记
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
flag = true;//发生交换
}
}
}
}
2.快速排序
快速排序(Quick Sort)是一种高效的排序算法,其基本思想是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
最关键的partion函数实现:
以下是快速排序的 C++ 代码实现:
#include <iostream>
using namespace std;
int partition(int arr[], int low, int high) {
int pivot = arr[low];
while (low < high) {
while (low < high && arr[high] >= pivot) {
high--;
}
arr[low] = arr[high];
while (low < high && arr[low] <= pivot) {
low++;
}
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pivot = partition(arr, low, high);
quickSort(arr, low, pivot - 1);
quickSort(arr, pivot + 1, high);
}
}
int main() {
int arr[] = {37, 23, 0, 17, 12, 72, 31, 46, 100, 88, 54};
int n = sizeof(arr) / sizeof(arr[0]);
quickSort(arr, 0, n - 1);
cout << "Sorted array is: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
这段代码首先定义了一个名为 partition
的函数,该函数用于将数组按照一个基准元素分成两部分,并返回基准元素的最终位置。然后定义了一个名为 quickSort
的函数,该函数用于递归地对数组进行快速排序。在 main
函数中,我们创建了一个待排序的整数数组,并调用 quickSort
对数组进行排序。最后,我们输出排序后的数组。
最优情况下:快速排序算法的时间复杂度为O(nlogn).
快速排序不是原地排序,是一种不稳定的排序方法
由于程序中使用了递归;需要递归调用栈的支持,而栈的长度取决于递归调用的深度。(即使不用递归,也需要用用户栈)
- 在平均情况下:需要O(logn)的栈空间
- 最坏情况下:栈空间可达O(n)
快速排序不适合对原本有序或基本有序的记录序列进行排序。
10.3 选择排序
1.简单选择排序
基本思想:在待排序的数据中选出最大(小)的元素放在其最终的位置。
选择排序(Selection Sort)是一种简单的排序算法,其基本思想是每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
以下是选择排序的 C++ 代码实现:
#include <iostream>
using namespace std;
void selectionSort(int arr[], int n) {
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;
}
}
if (minIndex != i) {
swap(arr[i], arr[minIndex]);
}
}
}
int main() {
int arr[] = {37, 23, 0, 17, 12, 72, 31, 46, 100, 88, 54};
int n = sizeof(arr) / sizeof(arr[0]);
selectionSort(arr, n);
cout << "Sorted array is: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
这段代码首先定义了一个名为 selectionSort
的函数,该函数用于对数组进行选择排序。在 main
函数中,我们创建了一个待排序的整数数组,并调用 selectionSort
对数组进行排序。最后,我们输出排序后的数组。
简单选择排序是不稳定排序。
2.堆排序
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
堆排序:
若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列重又建成一个堆,则得到n个元素的次小值(次大值)···如此反复,便能得到一个有序序列,这个过程称之为堆排序。
如何在输出堆顶元素后,调整剩余元素为一个新的堆?
堆排序具体过程可参考B站大佬视频:【排序算法:堆排序【图解+代码】】
#include<iostream>
using namespace std;
void heapify(int arr[], int n, int i) {
int largest = i; // Initialize largest as root
int left = 2 * i + 1; // left = 2*i + 1
int right = 2 * i + 2; // right = 2*i + 2
// If left child is larger than root
if (left < n && arr[left] > arr[largest])
largest = left;
// If right child is larger than largest so far
if (right < n && arr[right] > arr[largest])
largest = right;
// If largest is not root
if (largest != i) {
swap(arr[i], arr[largest]);
// Recursively heapify the affected sub-tree
heapify(arr, n, largest);
}
}
// main function to do heap sort
void heapSort(int arr[], int n) {
// Build heap (rearrange array)
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// One by one extract an element from heap
for (int i = n - 1; i > 0; i--) {
// Move current root to end
swap(arr[0], arr[i]);
// call max heapify on the reduced heap
heapify(arr, i, 0);
}
}
// print array
void printArray(int arr[], int n) {
for (int i = 0; i < n; ++i)
cout << arr[i] << " ";
cout << "\n";
}
// driver code
int main() {
int arr[] = { 12, 11, 13, 5, 6, 7 };
int n = sizeof(arr) / sizeof(arr[0]);
cout << "Before sorting: ";
printArray(arr, n);
heapSort(arr, n);
cout << "After sorting: ";
printArray(arr, n);
}
堆排序的时间主要耗费在建立初始堆和调整建新堆时进行的反复筛选。堆排序在最坏情况下,其时间复杂度也为O(nlogn),这时堆排序的最大优点。无论待排序序列中的记录时正序还是逆序排列,都不会使堆排序处于最好或最坏状态。
10.4 归并排序
归并一词的中文含义就是合并、并入的意思,而在数据结构中的定义是将两个或两个以上的有序表组合成一个新的有序表。
归并排序(Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,在额可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]([x]表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,······如此重复,直到得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
归并排序具体过程可参考B站大佬视频:【排序算法:归并排序【图解+代码】】
思路:分治+递归
#include <iostream>
#include <vector>
using namespace std;
void merge(vector<int>& arr, int left, int mid, int right) {
vector<int> temp(right - left + 1);
//标记左半区第一个未排序的元素i
//标记右半区第一个未排序的元素j
//临时数组元素下标k
int i = left, j = mid + 1, k = 0;
//合并
while (i <= mid && j <= right) {
if (arr[i] < arr[j])
temp[k++] = arr[i++];
else
temp[k++] = arr[j++];
}
//合并左半区剩余元素
while (i <= mid)
temp[k++] = arr[i++];
//合并右半区剩余元素
while (j <= right)
temp[k++] = arr[j++];
//把临时数组中合并后的元素添加回原来的数组
for (int p = 0; p < temp.size(); p++)
arr[left + p] = temp[p];
}
//归并排序
void mergeSort(vector<int>& arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid); //归并划分左半区
mergeSort(arr, mid + 1, right); //归并划分右半区
merge(arr, left, mid, right); //合并
}
}
int main() {
vector<int> arr = { 38, 27, 43, 3, 9, 82, 10 };
int n = arr.size();
mergeSort(arr, 0, n - 1);
for (int i = 0; i < n; i++)
cout << arr[i] << " ";
return 0;
}
10.5 基数排序
基数排序(Radix Sort)是一种非比较排序算法,它按照数字的每一位来排序。具体来说,它从最低位开始,将数字按照每一位进行排序,然后依次按照高位到低位排序,直到所有位都排好序。
基数排序的思想可以分为两个部分:分配和收集。
- 分配(Distribute):我们通过将数字放入桶(bucket)的方式来对数字进行排序。首先,我们准备好10个桶,然后遍历整个数组,将每个数字放入对应的桶中。这里,我们假设要排序的数字只包含于0-9之间。
- 收集(Collect):收集的过程与分配过程相反。我们从桶中取出数字,并按照桶的顺序将数字放回原数组中。这里,我们假设最高位在桶中的顺序为0-9,然后下一位的桶顺序为0-9。
基数排序的时间复杂度为O(d*(n+k)),其中d是数字的最大位数,n是数组的长度,k是每一位上数字的取值范围(这里是10)。由于k是一个常数,所以基数排序的时间复杂度可以表示为O(d*n)。
以下是基数排序算法的C++代码实现:
#include <iostream>
#include <vector>
// 获取数字的某一位上的值
int getDigit(int num, int digit) {
for (int i = 0; i < digit - 1; i++) {
num /= 10;
}
return num % 10;
}
// 基数排序函数
void radixSort(std::vector<int>& arr) {
// 获取数组中的最大值
int max = arr[0];
for (int i = 1; i < arr.size(); i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 计算最大值的位数
int digits = 0;
while (max > 0) {
max /= 10;
digits++;
}
// 创建桶并进行排序
std::vector<std::vector<int>> buckets(10);
for (int i = 1; i <= digits; i++) {
// 将数字分配到桶中
for (int j = 0; j < arr.size(); j++) {
int digit = getDigit(arr[j], i);
buckets[digit].push_back(arr[j]);
}
// 从桶中收集数字
int index = 0;
for (int j = 0; j < buckets.size(); j++) {
for (int k = 0; k < buckets[j].size(); k++) {
arr[index++] = buckets[j][k];
}
buckets[j].clear();
}
}
}
int main() {
std::vector<int> arr = {170, 45, 75, 90, 802, 24, 2, 66};
radixSort(arr);
std::cout << "Sorted array: ";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
在此代码中,我们使用了桶来对每一位进行排序。首先,我们初始化一个大小为10的桶,用于存放数字。然后,我们从个位开始,依次按照每一位进行排序。对于每一位,我们将数组中的所有数字放入对应的桶中,然后依次将桶中的数字放回原数组。最后,我们重复这个过程,直到所有位数都排好序。时间复杂度为O(d*n),其中d是数字的最大位数。
10.6 排序总结
各种排序算法的比较:
各种排序方法的综合比较:
一、时间性能
1.按平均的时间性能,有三类排序方法:
时间复杂度为O(nlogn)的方法有:
- 快速排序、堆排序和归并排序,其中以快速排序为最好
时间复杂度为O(n^2)的方法有:
- 直接插入排序、冒泡排序和简单选择排序,其中以直接插入为最好
时间复杂度为O(n)的排序方法有:
- 基数排序
2.当待排序记录按关键字顺序有序时,直接插入排序和冒泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间复杂度退化为O(n^2),因此是应该尽量避免的情况。
3.简单选择排序、堆排序、和归并排序的时间性能不随记录序列中关键字的分布而改变。
二、空间性能
指的是排序过程中所需的辅助空间的大小
1.所有的简单排序算法(包括:直接插入、冒泡和简单选择)和堆排序的空间复杂度为O(1)
2.快速排序为O(logn),为栈空间所需的辅助空间
3.归并排序所需辅助空间最多,其空间复杂度为O(n)
4.链式基数排序需附设队列首位指针,则空间复杂度为O(rd)
三、排序方法的稳定性能
- 稳定的排序方法指的是,对两个关键字相等的记录,它们再序列中的相对位置,在排序之前和经过排序之后,没有改变。
- 当对多关键字的记录序列进行LSD方法(基数排序)排序时,必须采用稳定的排序方法。
- 对于不稳定的排序方法,只要能举出一个实例说明即可。
- 快速排序和堆排序时不稳定的排序方法。
四、关于排序方法的时间复杂度的下限
本章讨论的各种排序方法,除基数排序外,其他方法都是基于“比较关键字”进行排序的排序方法。可以证明,这类排序法可能达到的最快时间复杂度为O(nlogn)。(基数排序不是基于比较关键字的排序方法,所以它不受这个限制)
可以用一棵判定树来描述这类基于“比较关键字”进行排序的排序方法。
直接插入排序和冒泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间复杂度退化为O(n^2),因此是应该尽量避免的情况。