排序算法(Sort Algorithm)
冒泡排序
- Bubble Sort —— 冒泡排序
简介(Introduction)
它重复地走访过要排序的元素列,依次 比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行,直到没有相邻元素需要交换,也就是说该元素列已经排序完成
描述(Description)
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数
- 针对所有的元素重复以上的步骤,除了最后一个
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
- 时间复杂度:
- 最好:\(O(n)\)
- 平均:\(O(n^2)\)
- 最坏:\(O(n^2)\)
- 空间复杂度:\(O(1)\)
- 稳定
代码(Code)
// C++ Version
void bubble_sort(int arr[], int n) {
for (int i = 0; i < n - 1; i ++ ) {
bool flag = false;
for (int j = n - 1; j > i; j -- )
if (arr[j] < arr[j - 1]) {
swap(arr[j], arr[j - 1]);
flag = true;
}
if (!flag) break;
}
}
选择排序
- Selection Sort —— 选择排序
简介(Introduction)
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零
描述(Description)
- 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 以此类推,直到所有元素均排序完毕
- 时间复杂度:
- 最好:\(O(n^2)\)
- 平均:\(O(n^2)\)
- 最坏:\(O(n^2)\)
- 空间复杂度:\(O(1)\)
- 不稳定
代码(Code)
// C++ Version
void select_sort(int arr[], int n) {
for (int i = 0; i < n - 1; i ++ ) { // 代表排序要进行的躺数 数组长度 -1 次
int t = i; // 先假设数组中的最大值或者最小值的下标是 i 这里举例升序排列
for (int j = i + 1; j < n; j ++ ) // 每次要从假设的最小值下面的后面开始比较
if (arr[j] < arr[t])
t = j; // 如果后面的元素小于假设的最小值,更新下标
swap(a[t], a[i]);
}
}
插入排序
- Insertion Sort —— 插入排序
简介(Introduction)
将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动
描述(Description)
- 插入排序是指在待排序的元素中,假设前面 \(n-1\)(其中 \(n\ge2\) )个数已经是排好顺序的
- 将第 \(n\) 个数插到前面已经排好的序列中,找到合适自己的位置
- 使得插入第 \(n\) 个数的这个序列也是排好顺序的
- 按照此法对所有元素进行插入,直到整个序列排为有序的过程
- 时间复杂度:
- 最好:\(O(n)\)
- 平均:\(O(n^2)\)
- 最坏:\(O(n^2)\)
- 空间复杂度:\(O(1)\)
- 稳定
示例(Example)
代码(Code)
- 直接插入排序:
// C++ Version void insert_sort(int arr[], int n) { for (int i = 1; i < n; i ++ ) { int t = arr[i], j = i; while (j && q[j - 1] > t) { arr[j] = arr[j - 1]; j -- ; } arr[j] = t; } }
- 折半插入排序:
// C++ Version void binary_search_insert_sort(int arr[], int n) { for (int i = 1; i < n; i ++ ) { if (arr[i - 1] <= arr[i]) continue; int t = arr[i]; int l = 0, r = i - 1; while (l < r) { int mid = l + r >> 1; if (arr[mid] > t) r = mid; else l = mid + 1; } for (int j = i - 1; j >= r; j -- ) arr[j + 1] = arr[j]; arr[l] = t; } }
希尔排序
- Shell Sort —— 希尔排序
简介(Introduction)
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 \(1\) 时,整个文件恰被分成一组,算法结束
描述(Description)
- 希尔排序目的为了加快速度改进了插入排序,交换不相邻的元素对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
- 在此我们选择增量\(gap = \frac{length}{2}\)
- 缩小增量以 \(gap = \frac{gap }{2}\) 的方式,用序列 \(\left\{ \frac{n}{2},\ \frac{\frac{n}{2}}{2} \ \dots 1 \right\}\) 来表示。
- 时间复杂度:\(O(\large n^ \frac{3}{2})\)
- 空间复杂度:\(O(1)\)
- 不稳定
示例(Example)
代码(Code)
// C++ Version
void shell_sort(int arr[], int n) {
if (n == 2) {
if (arr[0] > arr[1]) swap(arr[0], arr[1]);
return 0;
}
for (int d = n / 3; d; d = d == 2 ? 1 : d / 3) { // 当增量为时,将增量设为1,否增增了每次缩小三倍
for (int start = 0; start < d; start ++ ) { // 分组数
for (int i = start + d; i < n; i += d) { // 增量内部使用插入排序
int t = q[i], j = i;
while (j > start && q[j - d] > t) {
q[j] = q[j - d];
j -= d;
}
q[j] = t;
}
}
}
}
// Java Version
// 希尔交换
private static void sort(int[] arr) {
int size = arr.length;
while (true) {
size = size / 2;
// 设置总遍历数即: 需要遍历 7,6,9,3四个元素
for (int i = 0; i < size; i++) {
//按照步长对数组进行遍历
for (int j = i + size; j < arr.length; j += size) {
//开始进行插入排序
for (int k = j - size; k >= 0; k -= size) {
if (arr[k] > arr[k + size]) {
swap(arr, k, k + size);
} else break;
}
}
}
//当 size 为1时,整个数组近乎有序
if (size == 1) {
break;
}
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
快速排序
- Quick Sort —— 快速排序
简介(Introduction)
快速排序由 C. A. R. Hoare 在1960年提出。
它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序是是对冒泡排序的一种改进,是非常重要且应用比较广泛的一种高效率排序算法
描述(Description)
- 设定一个分界值,通过该分界值将数组分成左右两部分
- 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于或等于分界值
- 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
- 重复上述过程,是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了
-
可以等长划分时效率最高,当近乎有序时效率最低,所以在快排后面的阶段,可以使用直接插入排序来进一步优化时间
-
若将相比较元素 \(tag\) 第一个元素,则第 \(i\) 趟排序完成时,会有 \(i\) 个以上的元素在最终的位置,其算法执行流程为:先 从后向前 扫描比 \(tag\) 小的元素,并交换,然后 从后向前 扫描比 \(tag\) 大的元素,并交换。
-
时间复杂度:
- 最好:\(O(n\log n)\)
- 平均:\(O(n\log n)\)
- 最坏:\(O(n^2)\)
-
空间复杂度:\(O(\log n)\)
-
不稳定
示例(Example)
代码(Code)
// C++ Version
int q[110];
void quick_sort(int q[], int l, int r) {
if (l >= r) return;
int i = l - 1, j = r + 1, mid = q[r + l >> 1];
while (i < j) {
while (q[++ i] < mid);
while (q[-- j] > mid);
if (i < j)swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
// Java Version
private static void sort(int[] arr, int left, int right) {
int pivot = arr[left];
int l = left, int r = right;
while (l < r) {
while ((l < r) && pivot > arr[l]) l++;
while ((l < r) && arr[r] > pivot) r--;
if (arr[l] == arr[r] && (l < r)) l++;
else swap(arr, l, r);
}
if (l - 1 > left) sort(arr, left, l - 1);
if (r + 1 < right) sort(arr, r + 1, right);
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
归并排序
- Merge Sort —— 归并排序
简介(introduction)
归并排序 是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用 分治法 ( Divide and Conquer) 的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
描述(Description)
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤 \(3\) 直到某一指针超出序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
- 时间复杂度:
- 最好:\(O(n\log n)\)
- 平均:\(O(n\log n)\)
- 最坏:\(O(n\log n)\)
- 空间复杂度:\(O(n)\)
- 稳定
示例(Example)
代码(Code)
// C++ Version
int q[110], tmp[110];
void merge_sort(int q[], int l, int r) {
if (l >= r) return;
int mid = (r - l) / 2 + l;
merge_sort(q, l, mid); //左递归处理
merge_sort(q, mid + 1, r); //右递归处理
int idx = 0, i = l, j = mid + 1;
while (i <= mid && j <= r) {
if (q[i] < q[j]) tmp[idx ++ ] = q[i ++ ]; //数组tmp作为临时储存空间
else tmp[idx ++ ] = q[j ++ ];
}
//左右两者其中一个未遍历完成的直接把剩下整个接到数组后面
while (i <= mid) tmp[idx ++ ] = q[i ++ ];
while (j <= r) tmp[idx ++ ] = q[j ++ ];
for (int i = l, j = 0; i <= r; i ++ , j ++ ) q[i] = tmp[j]; //返回给a数组
}
// Java Version
//分组
public static void mergetDiv(int[] arr, int left, int right, int[] temp) {
if (left < right) { //在相等时候,即:已经将数组分割成一个单位的数组
int mid = (int) ((right - left) / 2 + left);
//向左进行分割
mergetDiv(arr, left, mid, temp);
//向右进行分割
mergetDiv(arr, mid + 1, right, temp);
//每次分割完后在栈底开始回溯合并
merget(arr, left, mid, right, temp);
}
}
//合并
public static void merget(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; //左边界的初始化
int j = mid + 1; //右边界初始化
int t = 0; //临时数组的下表
//左右边界判断大小填入temp数组
while (i <= mid && j <= right) { //结束标志判断
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t++; i++;
} else {
temp[t] = arr[j];
t++; j++;
}
}
//若存在多余元素则直接加入temp
while (i <= mid) {
temp[t] = arr[i];
i++; t++;
}
while (j <= right) {
temp[t] = arr[j];
j++; t++;
}
//将 temp 重新拷贝给arr数组
t = 0;
int leftIndex = left; //左边临时变量直接为本次合并的左端初始值
while (leftIndex <= right) {
arr[leftIndex] = temp[t];
leftIndex++; t++;
}
}
堆排序
- Heap Sort —— 堆排序
简介(Introduction)
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,是不稳定排序
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大根堆;
每个结点的值都小于或等于其左右孩子结点的值,称为小根堆
- 时间复杂度:
- 最好:\(O(n\log n)\)
- 平均:\(O(n\log n)\)
- 最坏:\(O(n\log n)\)
- 空间复杂度:\(O(\log n)\)
- 不稳定
描述(Description)
- 将待排序序列构造成一个大顶堆
- 此时,整个序列的最大值就是堆顶的根节点
- 将其与末尾元素进行交换,此时末尾就为最大值
- 然后将剩余 \(n - 1\) 个元素重新构造成一个堆,这样会得到 \(n\) 个元素的次小值。
- 反复执行,便能得到一个有序序列
可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了
Tips: 升序采用小根堆,降序采用大根堆
示例(Example)
- 要求:给你一个数组 \(\left[ 4, 6, 8, 5, 9 \right]\) , 要求使用堆排序法,将数组升序排序。
-
步骤一: 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)
原始的数组 \(\left[ 4, 6, 8, 5, 9 \right]\)
- 假设给定无序序列结构如下
- 此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 \(\frac{arr.length}{2} - 1 = \frac{5} {2} - 1 = 1\),也就是下面的 \(6\) 结点),从左至右,从下至上进行调整。
- 找到第二个非叶节点 \(4\),由于 \(\left[ 4,9,8 \right]\) 中 \(9\) 元素最大,\(4\) 和 \(9\) 交换
- 这时,交换导致了子根 \(\left[ 4,5,6 \right]\) 结构混乱,继续调整, \(\left[ 4,5,6 \right]\) 中 \(6\) 最大,交换 \(4\) 和 \(6\)
- 此时,我们就将一个无序序列构造成了一个大顶堆
-
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换
- 将堆顶元素 \(9\) 和末尾元素 \(4\) 进行交换
- 重新调整结构,使其继续满足堆定义
- 再将堆顶元素 \(8\) 与末尾元素 \(5\) 进行交换,得到第二大元素 \(8\)
- 后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
代码(Code)
// C++ Version
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int n, sz;
int h[N];
void down(int x) {
int t = x;
//找到左右子节点与根节点的最小值
if (2 * t < sz && h[2 * t] < h[t]) t = 2 * x;
if (2 * t + 1 < sz && h[2 * t + 1] < h[t]) t = 2 * t + 1;
if (t != x) {
swap(h[x], h[t]); //交换
down(t); //递归
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
sz = n;
//建堆
for (int i = n / 2; i; i -- ) down(i);
while (n -- ) {
printf("%d ", h[1]);
h[1] = h[size--];
down(1);
}
return 0;
}
// Java Version
public static void heap(int[] arr) {
int temp = 0;
//首先:将无需的数组构成一个大顶堆/小顶堆(根据 升序/降序 进行排列)
for (int i = arr.length / 2 + 1; i >= 0; i--) {
changeHeap(arr, i, arr.length);
}
for (int i = arr.length - 1; i > 0; i--) {
//将大顶堆第一个值与大顶堆最后一个值进行交换
temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
//循环进行排序比较
changeHeap(arr,0,i);
}
System.out.println(Arrays.toString(arr));
}
/**
* 将元素进行交换,成为一个大顶堆
* @param arr 待排序数组
* @param index 第一个非叶子节点
* @param length 数组长度
*/
public static void changeHeap(int[] arr, int index, int length) {
int temp = arr[index];
for (int i = index * 2 + 1; i < length; i = i * 2 + 1) {
//左子节点小于于右子节点
if (i + 1 < length && arr[i] < arr[i + 1]) {
i++;//找到子最大的子节点
}
if (arr[i] > temp) { //如果子节点大于父节点
arr[index] = arr[i]; //交换
index = i; //对index进行重新赋值
} else {
break; //自下而上进行寻找,如果当前已经不需要进行交换,说明下面已经是大顶堆
}
}
arr[index] = temp; //将 temp 重新赋值给 arr[index] 位置
}
桶排序
- Bucket Sort —— 桶排序
简介(Introduction)
桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
描述(Description)
- \(a_i\) 排序的位置, 取决于 小于 \(a_i\) 的元素的个数和 \(a_i\) 相等 的元素的个数
- 如果有元素相同, 则 从后往前排序 就可以保证值相同的元素相对位置不发生变化
- \(\le a_i\) 的元素的个数可以通过 前缀和 求出
- 时间复杂度:
- 最好:\(O(n + m)\)
- 平均:\(O(n + m)\)
- 最坏:\(O(n + m)\)
- 空间复杂度:\(O(n + m)\)
- 稳定
示例(Example)
代码(Code)
// C++ Version
int s[110], w[110];
void bucker_sort(int arr[], int n) {
for (int i = 0; i < n; i ++ ) s[a[i]] ++ ; // 统计每个元素出现次数
for (int i = 1; i < N; i ++ ) s[i] += s[i - 1]; // 前缀和
for (int i = n - 1; i >= 0; i -- ) w[ -- s[a[i]]] = a[i]; // 从后往前找到对应的位置
for (int i = 0; i < n; i ++ ) a[i] = w[i]; // 将元素重新还给 a 数组
}
基数排序
- Radix Sort —— 基数排序
简介(Introduction)
- 通过键值得各个位的值,将要排序的元素分配至一些桶中,达到排序的作用
- 基数排序法是属于稳定性的排序,基数排序法是效率高的稳定排序法
- 基数排序是桶排序的扩展
描述(Description)
- 确定数组中的最大元素有几位 \(max\)(确定执行的轮数)
- 创建 $0 \sim 9 $ 个桶(以十进制为例),因为所有的数字元素都是由 $0 \sim 9 $ 的十个数字组成
- 依次判断每个元素的个位,十位至 \(max\) 位,存入对应的桶中,存入原数组,至 \(max\) 轮结束输出数组。
- 简单来说,基数排序就是按照关键字组数进行了多次的桶排序
- 时间复杂度:
- 最好:\(O(d(n + r))\)
- 平均:\(O(d(n + r))\)
- 最坏:\(O(d(n + r))\)
- 空间复杂度:\(O(n + r)\)
- 稳定
示例(Example)
代码(Code)
// C++ Version
// d:关键字的数量,也就是最多有多少位 r:进位制的基数:如2进制,十进制等等
void radix_sort(int d, int r) {
// radix 进制,从个位开始
int radix = 1;
//枚举所有的关键字
for (int i = 1; i <= d; i ++ ) {
for(int j = 0 ; j < r ; j ++ ) s[j] = 0 ; // 清空所有的桶
// 统计每个桶里面有多少个元素
for (int j = 0 ; j < n ; j ++ ) s[q[j] / radix % r] ++ ; // q[j] / radix % r:获得对应位上的数,计数
for (int j = 1 ; j < r ; j ++ ) s[j] += s[j - 1];// 求前缀和
for (int j = n - 1; j >= 0 ; j -- ) w[ -- s[q[j] / radix % r]] = q[j]; // 按当前位数上的大小从后往前排序
for(int j = 0 ; j < n ; j ++ ) q[j] = w[j]; // 赋值回原数组
radix *= r; // 做完当前个位后,往十位、百位上继续,直到最大位数都排完
}
}