数据结构基础之排序算法二
学习算法,排序算法当然是不能少的,这次我们来学习一下基础的选择排序,冒泡排序,以及大名鼎鼎的快速排序。
选择排序
选择排序,非常好理解,就是找最小的数放到第一位,然后从第二个数开始找最小的数,放到第二个位置,然后从第三个数开始找最小的数,放到第三个位置… 。每次找最小的数,就是选择最小的数,这就是选择的含义。
看个图就明白了
第一次,找到最小的数字1,放到第一个位置上(当然原来在第一个位置的元素就交换到原来数字1的位置上了);第二次,从第二个位置也就是6开始,找到最小的数字2,放到第二个位置…
选择排序的逻辑非常简单,那么代码肯定也不复杂了
/**
* 选择排序
*
* 思路
* 每一次找一个最小的值,交换放到最左边的位置上。
* 重复这样找下去即可。
*
* 时间复杂度 O(N^2)
* 空间复杂度 O(n)
* 稳定性 不稳定
*/
public class SelectSortDemo {
public static void selectSort(int[] data){
if(null == data || data.length == 0 || data.length == 1){
return ;
}
for (int i = 0; i < data.length; i++) { //
int minIndex = i; //当前这一轮中的最小值下标
for (int j = i+1; j < data.length; j++) { //内层循环,找最小的数
if(data[minIndex] > data[j]){
minIndex = j;
}
}
if(minIndex != i){ //最小值,不是初始值,说明在后面有更小的值,那么进行交换
//这儿用一个加减法技巧来完成两个数的交换,省下一个临时交换变量。
data[i] = data[i] + data[minIndex];
data[minIndex] = data[i] - data[minIndex];
data[i] = data[i] - data[minIndex];
}
}
}
public static void main(String[] args) {
int[] data = {2,6,3,5,4,1,0,8,7,9,2};
selectSort(data);
System.out.println(Arrays.toString(data));
}
}
注意代码中,交换两个位置上的值,有一个小技巧就是不使用辅助变量也可以完成的,你可以get一下。
选择的排序的时间复杂度显然是n^2,和插入排序一样,胜在简单,但是因为其不稳定的特性,实际使用的较少。
冒泡排序
冒泡排序,一次循环会找出一个最大数放到最后,就像水泡从水底往上冒出来一样,这也是冒泡排序的名称由来。它的基本逻辑是,挨着的两个元素比较,大的就就交换到后面,依次不断的往后比较,一轮下来,最大的那个数就会跑到最后面去。
同样,看图说话
代码实现也比较容易:
/**
* 冒泡排序
*
* 思路:
* 从前到后,依次两两比较,如果前面大于后面的元素,将两个元素进行交换,完成一轮比较之后,最大的值就会放到最后
* 重复这个过程,直到剩下最左边最后一个元素。
* 就像气泡从水底往上冒出来一样,这也是冒泡排序的名称由来。
*
* 时间复杂度 O(N^2)
* 空间复杂度 O(n)
* 稳定性 稳定(实现中在与有序段比较时,只有小于才发生交换)
*
*/
public class BubbleSortDemo {
public static void bubbleSort(int[] data){
if(null == data || data.length == 0 || data.length == 1){
return ;
}
for (int i = data.length; i > 0; i--) {
for (int j = 0; j < i-1; j++) {
if(data[j+1] < data[j] ){ // 后一个元素小于前一个元素,发生交换
int temp = data[j];
data[j] = data[j+1];
data[j+1] = temp;
}
}
}
}
public static void main(String[] args) {
int[] data = {2,6,3,5,4,1,7,0,9};
bubbleSort(data);
System.out.println(Arrays.toString(data));
}
}
时间复杂度显然是n^2,也是比较简单。
快速排序
快速排序,听名字就知道它很快,那么我们就来分析分析,它有多快,是如何快起来的?
快速排序同样使用了分治的思想,但和归并不一样的是,它在拆分时,并不是二分,而是选定一个基准值,将小于这个基准值的拆到一段,大于基准值的拆到一段;然后小的这一段再重新选择基准值,小于新基准值的再拆分一段,大于新基准值的拆为一段;重复这样下去,直到不用再拆为止。因为每次拆分之前,小于基准和大于基准的就分开了,那么最后拆出来的就已经是有序的了,因此它只有递归的递的过程,没有归的过程,不就是个尾递归嘛。
不说楞个多,上图才是正事
快排的核心思想相信你应该有所了解了,其中讲到选定一个基准值,将小于这个基准值的拆到一段,大于基准值的拆到一段,这个操作有很多种实现方式,上图就提供一种思路:
- 从基准值后面的元素中找出第一个比他小的元素,和基准值交换
- 从前面找出第一个比它大的元素,和基准值交换
- 重复1和2,直到遍历完这一段元素。
上一盘代码,你说香不香
/**
* 快速排序
*
* 思路: 拆分,递归
* 选择一个基准值,并通过比较和交换,将所有小于基准值的放到它的左边,将所有大于基准值的放到它的右边。
* (具体实现方式,可以先从基准值的右边找比他小的,和它交换; 然后从左边找比它大的,再和它交换;
* 然后再右边,再左边,做完一轮之后,就能实现基准值的左边都是小于它的,右边都是大于它的)。
* 现在就可以将数组拆分为3个部分(如果基准值最大/最小,那会是2个部分),左边的子数组,基准值,右边的子数组,
* 接下来迭代对左、右子数组进行一样的操作(选基准值,比较交换),直到拆分到不能再拆分为止(小于等于2个元素即可)
*
* 时间复杂度 O(nlogn)
* 空间复杂度 O(n)
* 稳定性 不稳定(因为以基准值进行交换,会打乱原本相同值的前后顺序)
*/
public class QuickSortDemo {
public static void quickSort(int[] data){
if(null == data || data.length == 0 || data.length == 1){
return ;
}
recursionSort(data, 0, data.length-1);
}
public static void recursionSort(int[] data, int left, int right){
//递归终止条件
if(left>=right){
return;
}
int base = left; //选定基准值,这儿简单起见,选左边第一个
int ll = left; //遍历左边时使用的指针
int rr = right; //遍历右边时使用的指针
while(ll < rr){
//从右边往左边找,找到一个比base小的值时,交换
while(ll < rr) {
if (data[rr] < data[base]) {
int temp = data[rr];
data[rr] = data[base];
data[base] = temp;
base = rr;
rr--;
break;
}
rr--;
}
//从左边往右边找,找到一个比base大的值时,交换
while(ll < rr) {
if (data[ll] > data[base]) {
int temp = data[ll];
data[ll] = data[base];
data[base] = temp;
base = ll;
ll++;
break;
}
ll++;
}
}
recursionSort(data, left, base-1); //左半边递归
recursionSort(data, base+1, right); //右半边递归
}
public static void main(String[] args) {
int[] data = {2,6,3,5,4,1,0,8,7,9,2};
quickSort(data);
System.out.println(Arrays.toString(data));
}
}
应该可以发现,快排中,基准值的选择还是比较重要的,选得好,那么拆分就比较均匀,树的深度也就越小,当然效率也就越高了,所以优化快排的思路就在基准值的选取上,你可以暂开。
到此呢,我们学习了6种排序算法了,来综合比较下他们:
排序名称 | 时间复杂度 | 是否稳定 | 额外空间开销 |
---|---|---|---|
插入排序 | O(n^2) | 稳定 | O(1) |
冒泡排序 | O(n^2) | 稳定 | O(1) |
选择排序 | O(n^2) | 不稳定 | O(1) |
希尔排序 | O(n^2) | 不稳定 | O(1) |
归并排序 | O(nlogn) | 稳定 | O(n) |
快速排序 | O(nlogn) | 不稳定 | O(1) |
这么多种排序算法我们究竟应该怎么选择呢?
1.分析场景:稳定还是不稳定
2.数据量:数据量小的时候选什么?比如就50个数,优先选插入。
综上所述,没有一个固定的排序算法,都是要根据情况分析的。当然,你不想去做过多选择,那么就上归并或者快排吧,至少在数据量大的时候,系统不会崩盘。
最后今天的几种排序算法,你可以在一个堪称神器的网站上观察排序算法运行的过程。
可视化动态排序演示:https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html
学了排序算法,来个思考题,对200W学生的成绩排序,成绩从0到999.99(两位小数),要求效率要高的。
(也许根本不是用排序来做哦,还记得年龄问题吗?扩展一下就可以变成计数排序了)