章节目录:
- 一、排序算法
- 1.1 概述
- 1.2 分类
- 1.3 算法复杂度
- 1.4 时间复杂度
- 1.5 空间复杂度
- 二、冒泡排序
- 2.1 概述
- 2.2 算法分析
- 2.3 代码示例
- 三、选择排序
- 3.1 概述
- 3.2 算法分析
- 3.3 代码示例
- 四、插入排序
- 4.1 概述
- 4.2 算法分析
- 4.3 代码示例
- 五、希尔排序
- 5.1 概述
- 5.2 算法分析
- 5.3 代码示例
- 六、快速排序
- 6.1 概述
- 6.2 算法分析
- 6.3 代码示例
- 七、归并排序
- 7.1 概述
- 7.2 算法分析
- 7.3 代码示例
- 八、基数排序
- 8.1 概述
- 8.2 算法分析
- 8.3 代码示例
- 九、排序总结与对比
- 十、结束语
一、排序算法
1.1 概述
- 排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
- 它是《数据结构与算法》中最基本的算法之一。
1.2 分类
-
内部排序:指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
-
外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。
-
示意图:
1.3 算法复杂度
算法的复杂性体现在运行该算法时的计算机所需资源的多少上,计算机资源最重要的是时间和空间(即寄存器)资源,因此复杂度分为时间和空间复杂度。
- 时间复杂度是指执行算法所需要的计算工作量。
- 空间复杂度是指执行这个算法所需要的内存空间。
1.4 时间复杂度
-
在计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。
-
一般情况下,算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数,用 T(n)表示,若有某个辅 助函数 f(n),使得当 n 趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称 f(n)是 T(n)的同数量级函数。 记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。
-
T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的 T(n) 不同,但时间复杂 度相同,都为 O(n²)。
-
计算方法:
-
用常数 1 代替运行时间中的所有加法常数: T(n)=n²+7n+6 => T(n)=n²+7n+1
-
修改后的运行次数函数中,只保留最高阶项 :T(n)=n²+7n+1 => T(n) = n²
-
去除最高阶项的系数: T(n) = n² => T(n) = n² => O(n²)
-
-
常见的时间复杂度:
- 常数阶 O(1)
- 对数阶 O(log2n)
- 线性阶 O(n)
- 线性对数阶 O(nlog2n)
- 平方阶 O(n^2)
- 立方阶 O(n^3)
- k 次方阶 O(n^k)
- 指数阶 O(2^n)
-
示意图:
- 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) < Ο(2n) ,随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
- 从图中可见,我们应该尽可能避免使用指数阶的算法。
1.5 空间复杂度
- 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模 n 的函数。
- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模 n 有关,它随着 n 的增大而增大,当 n 较大时,将占用较多的存储单元,例如快速排序和归并排序算法, 基数排序就属于这种情况。
- 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(
redis
、memcache
)和算法(基数排序)本质就是用空间换时间。
二、冒泡排序
2.1 概述
- 冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
- 这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
- 示意图:
2.2 算法分析
- 比较相邻的元素,如果第一个比第二个大,就交换它们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2.3 代码示例
备注:这里我准备了一个产生随机整数数组的方法,是为了用于测试验证。
public class SortUtil {
private SortUtil() {
}
private static final SecureRandom RANDOM = new SecureRandom();
/**
* 得到随机整数数组。
*
* @param num 需要多少个随机数
* @param bound 随机数的边界值
* @return {@link int[]}
*/
public static int[] getRandomArray(int num, int bound) {
int[] array = new int[num];
for (int i = 0; i < array.length; i++) {
int r = RANDOM.nextInt(bound);
array[i] = r;
}
return array;
}
}
- 冒泡排序实现:
public class BubbleSort {
public static void main(String[] args) {
// 创建长度为10,由100以内随机整数组成的数组。
int[] array = SortUtil.getRandomArray(10, 100);
System.out.println(Arrays.toString(array));
// [67, 92, 42, 79, 45, 7, 62, 40, 94, 13]
bubbleSort(array);
System.out.println(Arrays.toString(array));
// [7, 13, 40, 42, 45, 62, 67, 79, 92, 94]
}
public static void bubbleSort(int[] array) {
int len = array.length - 1;
int temp;
boolean notChange;
for (int i = 0; i < len; i++) {
notChange = true;
for (int j = 0; j < (len - i); j++) {
// 与后一位数进行比较,如果大于后一位数则向后移动。
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
// 本次循环,位置发生了变化。
notChange = false;
}
}
if (notChange) {
break;
}
}
}
}
三、选择排序
3.1 概述
- 选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。
- 所以用到它的时候,数据规模越小越好。唯一的好处就是不占用额外的内存空间。
- 示意图:
3.2 算法分析
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
3.3 代码示例
public class SelectionSort {
public static void main(String[] args) {
int[] array = SortUtil.getRandomArray(10, 100);
System.out.println(Arrays.toString(array));
// [42, 27, 13, 64, 87, 77, 96, 95, 5, 39]
selectionSort(array);
System.out.println(Arrays.toString(array));
// [5, 13, 27, 39, 42, 64, 77, 87, 95, 96]
}
public static void selectionSort(int[] array) {
int minIndex;
int temp;
for (int i = 0; i < array.length - 1; i++) {
// 假设当前为最小索引。
minIndex = i;
// 从当前假设的索引开始往后找。
for (int j = (i + 1); j < array.length; j++) {
if (array[j] < array[minIndex]) {
// 找到最小索引。
minIndex = j;
}
}
temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}
}
}
四、插入排序
4.1 概述
- 插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。
- 插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
- 示意图:
4.2 算法分析
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
4.3 代码示例
public class InsertSort {
public static void main(String[] args) {
int[] array = SortUtil.getRandomArray(10, 100);
System.out.println(Arrays.toString(array));
// [2, 74, 96, 10, 50, 49, 82, 27, 86, 59]
insertSort(array);
System.out.println(Arrays.toString(array));
// [2, 10, 27, 49, 50, 59, 74, 82, 86, 96]
}
public static void insertSort(int[] array) {
int cur;
int preIndex;
// 从第二个元素开始。
for (int i = 1; i < array.length; i++) {
cur = array[i];
preIndex = i - 1;
// 寻找插入位置。
while (preIndex >= 0 && cur < array[preIndex]) {
array[preIndex + 1] = array[preIndex];
preIndex--;
}
if (preIndex + 1 != i) {
array[preIndex + 1] = cur;
}
}
}
}
五、希尔排序
5.1 概述
- 1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。
- 希尔排序又叫缩小增量排序。
- 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
- 示意图:
5.2 算法分析
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
5.3 代码示例
public class ShellSort {
public static void main(String[] args) {
int[] array = SortUtil.getRandomArray(10, 100);
System.out.println(Arrays.toString(array));
// [9, 4, 20, 20, 84, 94, 74, 15, 31, 46]
shellSort(array);
System.out.println(Arrays.toString(array));
// [4, 9, 15, 20, 20, 31, 46, 74, 84, 94]
}
public static void shellSort(int[] array) {
int temp;
// 分割。
for (int gap = (array.length / 2); gap > 0; gap /= 2) {
// 分好的每组元素。
for (int i = gap; i < array.length; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
// 换位。
if (array[j] > array[j + gap]){
temp = array[j];
array[j] = array[j + gap];
array[j + gap] = temp;
}
}
}
}
}
}
六、快速排序
6.1 概述
- 快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
- 示意图:
6.2 算法分析
备注:上述示意图演示的基准为最左端黄色高亮标记值,而接下来的代码示例为中间值。
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆放在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
6.3 代码示例
public class QuickSort {
public static void main(String[] args) {
int[] array = SortUtil.getRandomArray(10, 100);
System.out.println(Arrays.toString(array));
// [22, 2, 97, 28, 92, 97, 47, 48, 3, 62]
quickSort(array, 0, array.length - 1);
System.out.println(Arrays.toString(array));
// [2, 3, 22, 28, 47, 48, 62, 92, 97, 97]
}
public static void quickSort(int[] array, int left, int right) {
// 左右下标及中轴值。
int l = left;
int r = right;
int pivot = array[(left + right) / 2];
int temp;
// 按照左边小于中轴值,右边大于中轴值进行分区。
while (l < r) {
while (array[l] < pivot) {
l++;
}
while (array[r] > pivot) {
r--;
}
// 分区结束,退出循环。
if (l >= r) {
break;
}
// 交换值。
temp = array[l];
array[l] = array[r];
array[r] = temp;
// 如果左边值与中轴值相等,则前移;右边值与中轴值相等,则后移。
if (array[l] == pivot) {
r--;
}
if (array[r] == pivot) {
l++;
}
}
// 避免栈溢出。
if (l == r) {
r--;
l++;
}
// 向左向右进行递归。
if (left < r) {
quickSort(array, left, r);
}
if (right > l) {
quickSort(array, l, right);
}
}
}
七、归并排序
7.1 概述
-
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
-
分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修
补"在一起,即分而治之。
-
示意图:
7.2 算法分析
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
7.3 代码示例
public class MergeSort {
public static void main(String[] args) {
int[] array = SortUtil.getRandomArray(10, 100);
System.out.println(Arrays.toString(array));
// [23, 43, 55, 86, 36, 22, 81, 87, 77, 50]
mergeSort(array, 0, array.length - 1);
System.out.println(Arrays.toString(array));
// [22, 23, 36, 43, 50, 55, 77, 81, 86, 87]
}
public static void mergeSort(int[] array, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
// 向左递归。
mergeSort(array, left, mid);
// 向右递归。
mergeSort(array, mid + 1, right);
// 合并。
merge(array, left, mid, right);
}
}
public static void merge(int[] array, int left, int mid, int right) {
// 准备临时数组及索引(用于存放归并过程的元素)。
int[] temp = new int[array.length];
int i = 0;
// 初始化左右起始索引。
int l = left;
int r = mid + 1;
//(一)
//先把左右两边(有序)的数据按照规则填充到temp数组,直到左右两边的有序序列,有一边处理完毕为止。
while (l <= mid && r <= right) {
//如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素,即将左边的当前元素,填充到 temp数组。
if (array[l] <= array[r]) {
temp[i] = array[l];
i++;
l++;
} else {
temp[i] = array[r];
i++;
r++;
}
}
//(二)
//把有剩余数据的一边的数据依次全部填充到temp
while (l <= mid) {
temp[i] = array[l];
i++;
l++;
}
while (r <= right) {
temp[i] = array[r];
i++;
r++;
}
//(三)
// 重置索引,将temp数组的元素拷贝到arr。
i = 0;
l = left;
while (l <= right) {
array[l] = temp[i];
i++;
l++;
}
}
}
八、基数排序
8.1 概述
-
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
-
示意图:
8.2 算法分析
-
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。
-
这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
8.3 代码示例
public class RadixSort {
public static void main(String[] args) {
int[] array = SortUtil.getRandomArray(10, 100);
System.out.println(Arrays.toString(array));
// [63, 53, 86, 90, 35, 85, 0, 31, 44, 88]
radixSort(array);
System.out.println(Arrays.toString(array));
// [0, 31, 35, 44, 53, 63, 85, 86, 88, 90]
}
public static void radixSort(int[] array) {
// 1.找到数组中最大值是几位数。
int max = array[0];
for (int i = 0; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
int maxValueLen = (max + "").length();
// 2.创建10个桶,每个桶的最大容量为数组长度。
int[][] bucket = new int[10][array.length];
// 3.记录桶中数据的个数。
int[] bucketCounts = new int[10];
for (int i = 0, n = 1; i < maxValueLen; i++, n *= 10) {
// 拿到每个每个值。
for (int j = 0; j < array.length; j++) {
// 对应位数的值。
int digit = array[j] / n % 10;
bucket[digit][bucketCounts[digit]] = array[j];
bucketCounts[digit]++;
}
int index = 0;
for (int k = 0; k < bucketCounts.length; k++) {
if (0 != bucketCounts[k]) {
//循环该桶即第k个桶(即第k个一维数组), 放入。
for (int l = 0; l < bucketCounts[k]; l++) {
//取出元素放入到arr。
array[index++] = bucket[k][l];
}
}
bucketCounts[k] = 0;
}
}
}
}
九、排序总结与对比
- 总结图:
- 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
- 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
- n: 数据规模。
- k: “桶”的个数。
- In-place: 不占用额外内存。
- Out-place: 占用额外内存。
十、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。