数据结构(二):排序算法
一、 排序算法概述
日常的程序设计中,排序是很常见的需求,把数据元素按照一定的规则进行排序,比如淘宝上的货品按照上架日期排序,百度的搜索按照最新的内容排序。
二、 冒泡排序
2.1排序原理
- 比较相邻的元素,如果前一个元素比后一个元素大,就交换两个元素的位置
- 第一次冒泡,最大的元素会被交换到最后的位置,第二次冒泡,第二大的元素会被交换到倒数第二的位置,以此类推,每次冒泡后参与冒泡的数量都会减少一个,直到最后参与冒泡的个数为1,则此时排序完成
2.2排序演示图
2.3代码实现
import java.util.Arrays;
public class Bubble {
public static void main(String[] args) { Integer[] list = new Integer[] {5,1,3,2,4}; Bubble.sort(list); System.out.println(Arrays.toString(list)); }
/** * 冒泡核心排序算法 * @param col */ private static void sort(Comparable[] col) { for(int i=col.length-1;i>0;i--) {//每次冒泡完都会减少一个数参与下一轮冒泡,直到剩最后一个数为止 for(int j=0;j<i;j++) {//前后元素的比较,由于每次冒泡完减少一个数参与比较,故j跟着i的数量走 if(bigger(col[j], col[j+1])) { switchPos(col,j,j+1); } } } }
/** * 前后元素的比较 * @param a * @param b * @return */ private static boolean bigger(Comparable a,Comparable b) { return a.compareTo(b)>0; }
/** * 元素位置的交换 * @param col * @param i * @param j */ private static void switchPos(Comparable[] col,int a,int b) { Comparable t = col[a]; col[a] = col[b]; col[b] = t; } } |
2.4时间复杂度分析
冒泡排序中,元素交换的执行次数为
(n-1)+(n-2)+…+2+1 = n^2/2-n/2
元素参与比较的执行次数为
(n-1)+(n-2)+…+2+1 = n^2/2-n/2
故冒泡的算法时间复杂度相加为n^2-n,根据大O计数法为O(n^2)
三、 选择排序
3.1排序原理
- 对于需要排序的数据对象中,假设第一个索引处的值为最小值,遍历过程中,当前索引处的值大于其他索引处的值时,认为其他索引处的值为最小值,并互相交换位置,直到遍历至数据对象的导数第二个数为止
3.2排序演示图
3.3代码实现
import java.util.Arrays;
public class Select {
public static void main(String[] args) { Integer[] list = new Integer[] {5,1,3,2,4}; Select.sort(list); System.out.println(Arrays.toString(list)); }
/** * 选择核心排序算法 * @param col */ private static void sort(Comparable[] col) { for(int i=0;i<col.length-1;i++) { int minIndex = i; for(int j=i+1;j<col.length;j++) { if(bigger(col[minIndex],col[j])) { minIndex = j; } } switchPos(col, i, minIndex); } }
/** * 前后元素的比较 * @param a * @param b * @return */ private static boolean bigger(Comparable a,Comparable b) { return a.compareTo(b)>0; }
/** * 元素位置的交换 * @param col * @param i * @param j */ private static void switchPos(Comparable[] col,int a,int b) { Comparable temp = col[a]; col[a] = col[b]; col[b] = temp; } } |
3.4时间复杂度分析
选择排序使用双层for循环,外层是数据交换,里层是数据比较
数据比较的次数是:(n-1)+(n-2)+…+2+1 = n^2/2-n/2
数据交换的次数是:n-1
故冒泡的算法时间复杂度相加为n^2/2+n/2-1,根据大O计数法为O(n^2)
四、 插入排序
4.1排序原理
- 把待排序的数据元素分成已排序和待排序的两组
- 找到未排序的第一个元素,向已排序的数据元素中插入
- 倒叙遍历已经排序的元素,直到找到比待插入元素小的元素,将待插入元素放在此位置,其余元素向后移动一位
4.2排序演示图
4.3代码实现
import java.util.Arrays;
public class Insert {
public static void main(String[] args) { Integer[] list = new Integer[] {5,1,3,2,4}; Insert.sort(list); System.out.println(Arrays.toString(list)); }
/** * 插入核心排序算法 * @param col */ private static void sort(Comparable[] col) { //外层为未排序对象 for(int i=1;i<col.length;i++) { //里层为已排序对象的倒序 for(int j=i;j>0;j--) { if(bigger(col[j-1], col[j])) { switchPos(col, j-1, j); }else { break; } } } }
/** * 前后元素的比较 * @param a * @param b * @return */ private static boolean bigger(Comparable a,Comparable b) { return a.compareTo(b)>0; }
/** * 元素位置的交换 * @param col * @param i * @param j */ private static void switchPos(Comparable[] col,int a,int b) { Comparable temp = col[a]; col[a] = col[b]; col[b] = temp; } } |
4.4时间复杂度分析
插入排序使用双层for循环,外层是未排序数据元素,里层是已排序数据元素
数据比较的次数是:(n-1)+(n-2)+…+2+1 = n^2/2-n/2
数据交换的次数是:(n-1)+(n-2)+…+2+1 = n^2/2-n/2
故冒泡的算法时间复杂度相加为n^2-n,根据大O计数法为O(n^2)
五、 希尔排序
5.1排序原理
- 选定一个增长量h,对数据对象按h的间隔进行分组,并对分组后的数据进行插入排序
- 减小增长量h直到剩下1,即排序成功
- 增长量h的选定无固定规则,经过科学论证h的规则存在多种可行的可能,没有绝对最优的规则,一般选择数组的长度n/2作为初始值,随后也是按n=n/2做递减
- 假设在插入排序中,有序的数组为2,3,5,6,无序数组为1,8,那么1和8的插入每次都得遍历2,3,5,6后才能做插入,而设定了增长量后,可以有效的减少遍历的次数,故希尔排序是插入排序的优化
5.2排序演示图
5.3代码实现
import java.util.Arrays;
public class Hill {
public static void main(String[] args) { Integer[] list = new Integer[] {5,1,3,2,4}; Hill.sort(list); System.out.println(Arrays.toString(list)); }
/** * 希尔核心排序算法 * @param col */ private static void sort(Comparable[] col) { int n = col.length; int h = 1; //初始增量 while(h<n/2) { h = h*2+1; }
while(h>=1) { //外层先找到待插入的元素,待插入的元素以h为起始索引 for(int i=h;i<n;i++) { //里层对待插入的元素,往前按h间隔找到待插入排序的数进行插入排序 for(int j=i;j>=h;j=j-h) { if(bigger(col[j-h],col[j])) { switchPos(col, j-h, j); } } h = h/2; } } }
/** * 前后元素的比较 * @param a * @param b * @return */ private static boolean bigger(Comparable a,Comparable b) { return a.compareTo(b)>0; }
/** * 元素位置的交换 * @param col * @param i * @param j */ private static void switchPos(Comparable[] col,int a,int b) { Comparable temp = col[a]; col[a] = col[b]; col[b] = temp; } } |
5.4时间复杂度分析
希尔排序的时间复杂度无法做清晰的计算,有人根据大量数据得出平均时间复杂度为O(n^1.3),我们可以做个实验如下,对于10w的大数据量情况下的排序,是不是希尔排序要优于插入排序
通过实际的测验证明,希尔排序的性能确实是优于插入排序的
六、 归并排序
6.1排序原理
如图,归并算法采用分治的思想,主要通过如下三步实现数据有序
- 将数据元素进行对半拆分,直到拆成只有1个数据元素的n组数据
- 对相邻的两组数进行排序,使之有序并合并
- 不断重复上一步,直到合成了一组有序的数据
- 归并排序是一种空间换时间的算法
6.2排序演示图
6.3代码实现
import java.util.Arrays;
public class Merge {
//辅助数组声明 private static Comparable[] assist;
public static void main(String[] args) { Integer[] list = new Integer[] {5,1,3,2,4}; Merge.sort(list); System.out.println(Arrays.toString(list)); }
/** * 归并核心排序算法 * @param col */ public static void sort(Comparable[] col) { int low = 0; int high = col.length-1; assist = new Comparable[col.length]; sort(col,low,high); }
private static void sort(Comparable[] col,int low,int high) { if(high<=low) { return; }
int mid = low + (high-low)/2;
//对左子组进行递归排序 sort(col,low,mid);
//对右子组进行递归排序 sort(col,mid+1,high);
//归并排序 merge(col,low,mid,high); }
/** * 对已排序的几组数据进行归并排序 * @param col * @param low * @param mid * @param high */ private static void merge(Comparable[] col, int low, int mid, int high) { int index = low; int p1 = low; //左右子组比对的左指针 int p2 = mid+1; //左右子组比对的右指针
//左右指针分别走,对比两个指针下的数据,小的往辅助数组里放 while(p1<=mid && p2<=high) { int leftNum = (int) col[p1]; int rightNum = (int) col[p2]; System.out.println("左数:"+leftNum+" 右数:"+rightNum); if(bigger(leftNum, rightNum)) { assist[index++] = col[p2++]; }else { assist[index++] = col[p1++]; } }
//右指针的数据比较完了,把左指针剩余的数填充到辅助数组中 while(p1<=mid) { assist[index++] = col[p1++]; }
//左指针的数据比较完了,把右指针剩余的数填充到辅助数组中 while(p2<=high) { assist[index++] = col[p2++]; }
for(int i=low;i<=high;i++) { col[i]=assist[i]; } }
/** * 前后元素的比较 * @param a * @param b * @return */ private static boolean bigger(Comparable a,Comparable b) { return a.compareTo(b)>0; }
/** * 元素位置的交换 * @param col * @param i * @param j */ private static void switchPos(Comparable[] col,int a,int b) { Comparable temp = col[a]; col[a] = col[b]; col[b] = temp; } } |
6.4时间复杂度分析
假设数据元素的个数为N,那么拆分有logN层,每层的最坏情况都是比较N次,所以算法复杂度为logN*N+logN,根据大O计数法为O(N*logN)
七、 快速排序
7.1排序原理
- 从数据元素中取一个值作为分界值,选取小于该分界值的元素作为左子组,选取大于该分解值的元素作为右子组
- 左右子组一直重复上述操作,直到子组的个数为1
7.2排序演示图
7.3代码实现
package com.data.struct.common.sort.quick;
import java.util.Arrays; import com.data.struct.common.sort.merge.Merge;
public class Quick {
public static void main(String[] args) { //Integer[] list = new Integer[] { 6,7,8,9,10,2,3,1}; Integer[] list = new Integer[] {3,1,2}; Quick.sort(list); System.out.println(Arrays.toString(list)); }
/** * 归并核心排序算法 * * @param col */ public static void sort(Comparable[] col) { int low = 0; int high = col.length - 1; sort(col, low, high); }
private static void sort(Comparable[] col, int low, int high) { if (high <= low) { return; }
System.out.println(Arrays.toString(col));
int quickIndex = quick(col, low, high);
// 对左子组进行递归排序 sort(col, low, quickIndex-1);
// 对右子组进行递归排序 sort(col, quickIndex+1, high); }
/** * 对已排序的几组数据进行归并排序 * * @param col * @param low * @param mid * @param high */ private static int quick(Comparable[] col, int low, int high) { //设定一个基准值,一般以第一个数为准 Comparable value = col[low]; //左指针起始位置 int leftPointer = low; //右指针起始位置 int rightPointer = high + 1;
while(true) { boolean flag = bigger(col[--rightPointer],value); while(flag) { //从右往左扫指针,发现比基准值小的值则停下 if(rightPointer<=low) { break; } }
while(bigger(value,col[++leftPointer])) { //从左往右扫指针,发现比基准值大的值则停下 if(leftPointer>=high) { break; } }
if(leftPointer>=rightPointer) { //左指针和右指针相遇,说明全部元素已扫描完成,跳出while循环 break; }else { switchPos(col, leftPointer, rightPointer); //左指针和右指针没相遇前,说明元素还没扫描完成,此时符合条件的元素交换位置 } }
//此时左边的元素小于基准值,右边的元素大于基准值,此时交换基准值和左右指针重叠处的值,基准值所在的索引,为新分组数据的分界线 switchPos(col, low, rightPointer); return rightPointer; }
/** * 前后元素的比较 * * @param a * @param b * @return */ private static boolean bigger(Comparable a, Comparable b) { return a.compareTo(b) > 0; }
/** * 元素位置的交换 * * @param col * @param i * @param j */ private static void switchPos(Comparable[] col, int a, int b) { Comparable temp = col[a]; col[a] = col[b]; col[b] = temp; } } |
7.4时间复杂度分析
假设每次选中的基准值都是中位数,则会出现如图的情况,切分次数为logN次,每次交换元素的操作为N/2,所以最优情况下的算法复杂度为N/2*logN次,根据大O计算法推算出为O(N*logN)
假设每次选中的基准值都是最大值或最小值,则每次左或右指针都得全量扫描,即N次,切分次数因此也需要N次,如果有数字为4,1,2,3,那么每次快排的结果如下
4123
3124
2134
1234
所以最坏情况下的快排结果为O(N^2)
7.5快速排序和归并排序的区别
- 归并排序是将若干数据元素切割成了最细粒度的左右子组,并对相邻子组进行排序,并合并。快速排序是将若干数据元素切割成了有序的左右子组,这样当所有左右子组都有序时,整个数据元素对象也就有序了。
- 在归并排序中,一个数组被等分为两半,递归调用发生在排序之前,在快速排序中,切分数组的位置取决于数组的内容,递归调用发生在排序之后
八、 排序的稳定性
在一组数据元素中,A和B相等,并且A元素排在B元素后面,经过一轮排序后,值相等的A和B的位置依然不变,此时排序是稳定的。
稳定性的意义:
如果一组数据元素只需要根据一种排序方式进行排序,此时排序稳定性是没有意义的,多种方式的排序,如几件商品同时进行价格,销量,库存等方式的排序,则此时排序稳定性是有意义的,如下:
按价格从高到低排序
按销量从高到低排序
排序算法的稳定性:
冒泡排序(稳定算法):冒泡排序中,通过相邻元素的大小比较,只有当前一个元素比后一个元素大时才交换位置,所以相等元素的位置保持不变,所以冒泡排序是稳定的算法
选择排序(不稳定算法):选择排序中,是假设第一个数为最小的,往后遍历发现比该数小的则交换位置,在5,6,5,2这一组数中,当第一个数遍历至元素2时,会发生元素交换,此时变成了2,6,5,5
由此判断选择排序是不稳定的
希尔排序(不稳定算法):希尔排序是按照一定增长量进行排序的算法,假设有一组数3,3,1,5,希尔的增长量为2,则在第一次插入排序后变成了1,3,3,5,由此判断希尔排序是不稳定的
归并排序(稳定算法):归并算法是虽然将数据元素拆分成n对,并且对相邻组的元素进行排序,但是只有在发生左指针扫到的元素,大于右指针扫的元素时,才将小的元素填充到辅助数组中
相等元素不会发生操作,所以归并排序是稳定的
快速排序(不稳定算法):快速排序是找一个基准值,左指针扫到比基准值小的放左边,右指针扫到比基准值大的放右边,若基准值找的数据元素中的中位数,如 4,3,2,2,1
第一轮快排后的位置是1,2,2,3,4,由此判断快速排序是不稳定的