常见排序算法
1、冒泡排序
2、插入排序
3、快速排序
4、归并排序
冒泡排序
冒泡过程
给定一个数组,循环遍历。从数组头部开始比较,每两个元素进行比较,一轮结束后,最大或者最小的元素放到了数组的末尾。然后再进行第二轮比较,以此类推,直到完成整个排序过程
代码实现
public static void sort(int[] numbers) {
int length = numbers.length;
for (int i = 0; i < length - 1; i++) {
for (int j = 0; j < length - 1 - i; j++) {
if (numbers[j] > numbers[j + 1]) {
int temp = numbers[j];
numbers[j] = numbers[j + 1];
numbers[j + 1] = temp;
}
}
}
}
插入排序
对数组 [2, 1, 7, 9, 5, 8] 进行插入排序
过程描述
首先将数组分成左右两个部分,左边是已经排好序的部分,右边是还没有排好序的部分,刚开始,左边已排好序的部分只有第一个元素2。接下来,我们对右边的元素一个一个进行处理,将它们放到左边。
先来看 1,由于 1 比 2 小,需要将 1 插入到 2 的前面,做法很简单,两两交换位置即可,[1, 2, 7, 9, 5, 8]。
然后,我们要把 7 插入到左边的部分,由于 7 已经比 2 大了,表明它是目前最大的元素,保持位置不变,[1, 2, 7, 9, 5, 8]。
同理,9 也不需要做位置变动,[1, 2, 7, 9, 5, 8]。
接下来,如何把 5 插入到合适的位置。首先比较 5 和 9,由于 5 比 9 小,两两交换,[1, 2, 7, 5, 9, 8],继续,由于 5 比 7 小,两两交换,[1, 2, 5, 7, 9, 8],最后,由于 5 比 2 大,此轮结束。
最后一个数是 8,由于 8 比 9 小,两两交换,[1, 2, 5, 7, 8, 9],再比较 7 和 8,发现 8 比 7 大,此轮结束。到此,插入排序完毕。
代码如下所示:
public static void insertSort(int[] numbers) {
int length = numbers.length;
for (int i = 1; i < length; i++) {
for (int j = i -1; j >= 0; j--) {
if (numbers[j + 1] < numbers[j]) {
swap(numbers, j, j + 1);
}
}
}
}
private static void swap(int[] numbers, int i, int j) {
int temp = numbers[i];
numbers[i] = numbers[j];
numbers[j] = temp;
}
快速排序
基本思想
快速排序也采用了分治的思想。
实现
把原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组。
举例:把班里的所有同学按照高矮顺序排成一排。
解法:老师先随机地挑选了同学 A,让所有其他同学和 A 比高矮,比 A 矮的都站在 A 的左边,比 A 高的都站在 A 的右边。接下来,老师分别从左边和右边的同学里选择了同学 B 和 C,然后不断地筛选和排列下去。
在分成较小和较大的两个子数组过程中,如何选定一个基准值(也就是同学 A、B、C 等)尤为关键。
例题分析
对数组 [2, 1, 7, 9, 5, 8] 进行排序。
解题思路
按照快速排序的思想,首先把数组筛选成较小和较大的两个子数组。
随机从数组里选取一个数作为基准值,比如 7,于是原始的数组就被分成了两个子数组。注意:快速排序是直接在原始数组里进行各种交换操作,所以当子数组被分割出来的时候,原始数组里的排列也被改变了。
接下来,在较小的子数组里选 2 作为基准值,在较大的子数组里选 8 作为基准值,继续分割子数组。
继续将元素个数大于 1 的子数组进行划分,当所有子数组里的元素个数都为 1 的时候,原始数组也被排好序了。
代码示例
主体函数代码如下。
void sort(int[] nums, int lo, int hi) {
if (lo >= hi) return; // 判断是否只剩下一个元素,是,则直接返回
// 利用 partition 函数找到一个随机的基准点
int p = partition(nums, lo, hi);
// 递归地对基准点左半边和右半边的数进行排序
sort(nums, lo, p - 1);
sort(nums, p + 1, hi);
}
下面用代码实现 partition 函数获得基准值。
int partition(int[] nums, int lo, int hi) {
// 随机选择一个数作为基准值,nums[hi] 就是基准值
swap(nums, randRange(lo, hi), hi);
int i, j;
// 从左到右用每个数和基准值比较,若比基准值小,则放到指针 i 所指向的位置。循环完毕后,i 指针之前的数都比基准值小
for (i = lo, j = lo; j < hi; j++) {
if (nums[j] <= nums[hi]) {
swap(nums, i++, j);
}
}
// 末尾的基准值放置到指针 i 的位置,i 指针之后的数都比基准值大
swap(nums, i, j);
//返回指针i,作为基准点的位置
return i;
}
算法分析 时间复杂度
- 最优情况:被选出来的基准值都是当前子数组的中间数。
这样的分割,能保证对于一个规模大小为 n 的问题,能被均匀分解成两个规模大小为 n/2 的子问题(归并排序也采用了相同的划分方法),时间复杂度就是:T(n) = 2×T(n/2) + O(n)。
把规模大小为 n 的问题分解成 n/2 的两个子问题时,和基准值进行了 n-1 次比较,复杂度就是 O(n)。很显然,在最优情况下,快速排序的复杂度也是O(nlogn)。
- 最坏情况:基准值选择了子数组里的最大或者最小值
每次都把子数组分成了两个更小的子数组,其中一个的长度为 1,另外一个的长度只比原子数组少 1。
举例:对于数组来说,每次挑选的基准值分别是 9、8、7、5、2。
解法:划分过程和冒泡排序的过程类似。
算法复杂度为O(n2)。
提示:可以通过随机地选取基准值来避免出现最坏的情况。
空间复杂度
和归并排序不同,快速排序在每次递归的过程中,只需要开辟 O(1) 的存储空间来完成交换操作实现直接对数组的修改,又因为递归次数为 logn,所以它的整体空间复杂度完全取决于压堆栈的次数,因此它的空间复杂度是 O(logn)。
每次随机选取一个基准值,将数组分成较小的一半和较大的一半,然后检查这个基准值最后所在的下标是不是 k,算法复杂度只需要 O(n)。
归并排序
基本思想
核心是分治,就是把一个复杂的问题分成两个或者多个相似的子问题,然后把子问题分成更小的子问题,直到子问题可以简单的直接求解,原始问题的解就是子问题解的合并。
实现
一开始先把数组从中间划分成两个子数组,一直递归地把子数组划分成更小的数组,直到子数组中只有一个元素,才开始排序
排序的方法就是按照大小顺序合并两个元素,接着以此按照递归的返回顺序,不断得合并排好序的子数组,直到最后把整个数组的顺序排号。
private static void sort(int[] A, int lo, int hi) {
//判断是否只剩下一个元素
if (lo >= hi) {
return;
}
//从中间将数组分成两个部分
int mid = lo + (hi - lo) / 2;
sort(A, lo, mid);
sort(A, mid + 1, hi);
marge(A, lo, mid, hi);
}
private static void marge(int[] nums, int lo, int mid, int hi) {
//复制一份原来的的数组
int[] copy = nums.clone();
//定义一个 k 指针表示从什么位置开始修改原来的数组,i 指针表示左半边的起始位置,j 表示右半边的起始位置
int k = lo, i = lo, j = mid + 1;
while (k <= hi) {
if (i > mid) {
nums[k++] = copy[j++];
} else if (j > hi) {
nums[k++] = copy[i++];
} else if (copy[j] < copy[i]) {
nums[k++] = copy[j++];
} else {
nums[k++] = copy[i++];
}
}
}
其中,While 语句比较,一共可能会出现四种情况。
左半边的数都处理完毕,只剩下右半边的数,只需要将右半边的数逐个拷贝过去。
右半边的数都处理完毕,只剩下左半边的数,只需要将左半边的数逐个拷贝过去就好。
右边的数小于左边的数,将右边的数拷贝到合适的位置,j 指针往前移动一位。
左边的数小于右边的数,将左边的数拷贝到合适的位置,i 指针往前移动一位。
本文内容来自拉勾教育《300分钟搞定数据结构与算法》 kaiwu.lagou.com/course/cour…