1、简介
排序是将元素按着指定关键字的大小递增或递减进行数据的排列,排序可以提高查找的效率
2、排序算法的分类
排序算法可大致分为四类七种,具体分类如下:
插入排序:直接插入排序、希尔排序
交换排序:冒泡排序、快速排序
选择排序:直接选择排序、堆排序
归并排序
3、插入排序
算法思想:每次将一个元素按着关键字大小插入到他前面已经排好序的子序列中,重复进行这个操作,直至插入所有数据。
3、1 直接插入排序
算法描述:假设前i个元素构成的子序列是有序的,然后根据关键字大小顺序将第i+1个元素a[i] 添加到已经排好序的子序列中,使得插入a[i]后的序列仍是一个有序序列,这样当元素都插入序列时,则排序完成。
算法实现:
/**
* 直接插入排序
* @author chaizepeng
*/
private static void directInsertSort1(int[] array) {
for (int i = 1; i < array.length; i++) {
int temp = array[i];//拿到下一个需要插入的数值
for (int j = i-1; j >= 0; j--) {//遍历已经排好序的集合
//将想要插入的元素和已经排好序的集合中的每一个进行比较
if (temp < array[j]) {
array[j+1] = array[j];
}else {
array[j+1] = temp;
break;
}
}
}
}
/**
* 直接插入排序
* @author chaizepeng
*/
private static void directInsertSort2(int[] array) {
for (int i = 1; i < array.length; i++) {
int temp = array[i];//拿到下一个需要插入的数值
int j ;
for (j = i-1; j >= 0 && temp < array[j]; j--) {//遍历已经排好序的集合
//将想要插入的元素和已经排好序的集合中的每一个进行比较
array[j+1] = array[j];
}
array[j+1] = temp;
}
}
算法分析:
假设一个序列{1,2,3,4,5},使用直接插入排序时,每个个元素需要比较1次,移动两次(现将第i个给temp,再将第temp给第i个)即可,则完成排序总需比较n-1次,移动2(n-1)次,时间复杂度是O(n)
再假设一个序列{5,4,3,2,1},使用直接插入排序时,第i个元素准备插入时,需要比较i-1次,移动2+(i-1)次(前一个元素往后移动一次,第i元素先给temp,然后temp再给第1个元素)也就是i+1次,所以n个元素总共许比较n(n-1)/2次,移动(n-1)(n+4)/2次,所以时间复杂度为O(n^2)
所以插入排序算法的时间复杂度在O(n)到O(n^2)之间,其排序效率与比较次数和元素长度直接相关。
3、2 希尔排序
算法描述:
代码实现:
/**
* 希尔排序
* @author chaizepeng
*/
private static void shellSort1(int[] array) {
int len = array.length;
int i,j,gap;
//步长每次除以2,使用步长来对数组进行分组
//步长的直接意思就是,每隔len个元素则为同一组元素,步长在数值上等于组数
for (gap = len / 2 ; gap > 0 ; gap /= 2){
//因为每次都是除以2,所以当步长越小时,每组中的元素越多,越接近于有序
//这里是遍历根据步长分割的每一组
for (i = 0 ; i < gap ; i++){
//将每组中的元素进行排序,j是在i+gap开始的,因为每隔gap个数便是同一组数据
//这是遍历每组中的每一个数据,组内数据进行比较,直接插入排序
//i+gap正好是获取到数组中的每一个数据
for (j = i + gap ; j < len ; j += gap){
if(array[j] < array[j - gap]){
int temp = array[j];
int k = j - gap;
while (k >= 0 && array[k] > temp){
array[k + gap] = array[k];
k -= gap;
}
array[k + gap] = temp;
}
}
}
}
}
/**
* 希尔排序
* @param array
*/
private static void shellSort2(int[] array){
int len = array.length;
int j , gap;
for (gap = len / 2 ; gap > 0 ; gap /= 2){
for (j = gap ; j < len ; j++){
if (array[j] < array[j - gap]){
int temp = array[j];
int k = j - gap;
while (k >= 0 && array[k] > temp){
array[k + gap] = array[k];
k -= gap;
}
array[k + gap] = temp;
}
}
}
}
/**
* 希尔排序
* @param array
*/
private static void shellSort3(int[] array) {
int i,j,gap;
int len = array.length;
for (gap = len / 2 ; gap > 0 ; gap /= 2){
for (i = gap ; i < len ; i++){
for (j = i - gap ; j>= 0 && array[j] > array[j+gap] ; j -= gap){
int temp = array[j];
array[j] = array[j+gap];
array[j+gap] = temp;
}
}
}
}
算法分析:
因为根据之前对直接插入排序的分析可以知道直接插入排序算法的效率与比较次数和元素长度直接相关,可以看出希尔排序是对直接插入排序算法做了优化,针对的就是比较次数和元素的长度,希尔排序讲究先分组排序,这样就见效了移动的次数,随着元素长度的增加,序列趋于有序,减少了比较的次数,降低了时间复杂度。另外,希尔排序的时间复杂度是O(n*(logn)^2)。
排序算法的稳定性:是对关键字相同的元素排序性能的描述,当两个元素A,B相等,排序之前A在B前边,如果排完排完序之后,A仍然在B前边,则说明排序算法是稳定的。自我感觉分析一个算法稳定还是不稳定是可行的,但是如果说一个算法是稳定还是不稳定的应该不准确吧?对于稳定性而言是不是就是对临界元素的一种处理方式而已呢?不必纠结。
4、交换排序
4、1 冒泡排序
算法描述:假设按着升序排序,就是从第一个元素开始比较相邻两个元素的值,如果前边的比后边的大,则交换两个值得位置,然后继续往后进行比较交换操作,一趟下来使得序列中最大的数在最后边,假设长度是n,则第一趟将最大的数放在第n个位置上,然后再进行第二趟,第二趟下来之后保证除第n个数之外最大数在第n-1的位置,然后继续下一趟,重复上边操作,直到不需要继续元素交换了便排序结束了。
算法实现:
/**
* 冒泡排序
* @author chaizepeng
*/
private static void bubbleSort1(int[] array) {
int len = array.length;
//控制比较的长度,最长len-1
for (int i = len-1; i > 0; i--) {
//第二层循环控制比较大小和交换位置
for (int j = 0; j < i; j++) {
if (array[j] > array[j+1]) {//如果前一个数比后一个数大,则交换位置
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
System.out.println("共有"+len+"个元素,这是第"+(len - i)+"趟");
}
}
/**
* 冒泡排序
* @author chaizepeng
*/
private static void bubbleSort2(int[] array) {
int len = array.length;
//外层循环控制遍历趟数,最多循环len-1次
for (int i = 0; i < len-1; i++) {
for (int j = 0; j < len - 1 - i; j++) {
//第二层循环控制比较大小和交换位置
if (array[j] > array[j+1]) {//如果前一个数比后一个数大,则交换位置
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
System.out.println("共有"+len+"个元素,这是第"+(i+1)+"趟");
}
}
/**
* 冒泡排序使用添加标记的方式进行优化,序列越接近有序,效率越高
* @author chaizepeng
*/
private static void bubbleSort3(int[] array) {
int len = array.length;
//标记
boolean flag;
//外层循环控制遍历趟数,最多循环len-1次
for (int i = 0; i < len-1; i++) {
flag = true;
for (int j = 0; j < len - 1 - i; j++) {
//第二层循环控制比较大小和交换位置
if (array[j] > array[j+1]) {//如果前一个数比后一个数大,则交换位置
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
flag = false;
}
}
System.out.println("共有"+len+"个元素,这是第"+(i+1)+"趟");
//如果没有进行数据交换,也就是flag==true
if (flag) {
break;//不再继续下一趟
}
}
}
算法分析:冒泡排序记住一点,只要没有元素交换了,则排序完成。
例如序列{1,2,3,4,5},第一趟比较4次,没有元素移动,时间复杂度O(n)
再例如序列{5,4,3,2,1},第一趟比较4次,移动12次,时间复杂度为O(n^2)
所以冒泡排序算法的时间复杂度在O(n)到O(n^2)之间,其时间复杂度与序列是否有序有直接关系,并且算法是稳定的。此算法每次都借助了一个临时的中间元素,用来交换两个数。
4、2 快速排序
算法描述:快速排序是对冒泡排序的优化,快速排序第一趟就根据某个元素将序列分成两部分,一部分中所有数据比此元素小,另一部分的所有数比此元素大或等于此元素;然后再对这两部分分别进行分割,整个过程可以递归进行,直到序列有序。
算法实现:
/**
* 看图理解代码,一下子就全明白了
* @author chaizepeng
*/
private static void quickSort1(int[] array) {
int i = 0;
int j = array.length - 1;
quickSort(i,j,array);
}
/**
* 递归实现快排算法
* @author chaizepeng
*/
private static void quickSort(int i, int j, int[] array) {
int left = i;
int right = j;
int key = array[i];
while (i < j) {//只要不相等就循环比较,交换操作
//从右往左遍历
while(i < j && array[j] >= key) {
j--;
}
if (array[j] < key) {//每次交换的都是一个小于key的元素和key所在位置上的值,也就是key值
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
//从左往右遍历
while(i < j && array[i] <= key) {
i++;
}
//交换i处和j处的数据
if (array[i] > key) {//每次交换的都是一个大于key的元素和key所在位置上的值,也就是key值
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
//递归使用
if (i > left) {
quickSort(0, i, array);
}
if (j < right) {
quickSort(j + 1, array.length-1, array);
}
}
算法分析:
这里写的快排是不稳定,快速排序算法效率受标记值(基准值)的影响,假如每次选择的基准值都是最值的话,那么就会导致被分割的子序列是不平衡的(一个里边就一个元素,另一个里边是其余的n-1个元素),则需要比较的趟数就会增加,导致算法效率低下,快排的时间复杂度在O(nlogn)到O(n^2)之间。所以想要提高快排的效率就要保证每次找的基准值是当前序列的中间值。
快排的空间复杂度在O(logn)到O(n)之间
5、选择排序
5、1 直接选择排序
算法描述:以升序为例:从第一个元素开始,依次比较序列中的元素,将最小的元素放在第一个位置;接着在第二个元素开始,依次比较序列中的值,将最小的元素放在第二个位置,依次类推,直到排序结束。
算法实现:
/**
* 以升序为例:从第一个元素开始,依次比较序列中的元素,将最小的元素放在第一个位置;接着在第二个元素开始,依次比较序列中的值,将最小的元素放在第二个位置,依次类推,直到排序结束
* 直接选择排序
* @author chaizepeng
*/
private static void straightSelectSort(int[] array) {
//控制比较的趟数
for (int i = 0; i < array.length - 1; i++) {
int minFlag = i;//记录一下最小值所在的下标
for (int j = i+1; j < array.length; j++) {//控制从何处开始比较,比较到何处结束
if (array[j] < array[minFlag]) {//比较当前值和之前记录的最小下标对应的值
minFlag = j;
}
}
//将最小值放在最前边
if (minFlag != i) {
int temp = array[minFlag];//最小值
array[minFlag] = array[i];//将当前值赋给最小值所在的位置
array[i] = temp;//当前位置放最小值
}
}
}
算法分析:
直接选择排序,最多需要n-1趟,并且每一趟都需进行n-i此的比较,所以时间复杂度是O(n^2),并且直接选择排序是不稳定的算法。自我感觉这是最容易理解和实现的排序算法。
5、2 堆排序
算法描述:堆排序是对直接选择排序的一种优化,直接选择排序算法在每一趟比较两个数大小时, 只是比较大小没有做任何操作, 而堆排序针对此处做了优化,他在比较的同时也将其他的元素(不是最小的元素)做了相应调整。堆排序是先使用待排序的序列构建一个堆,这里用大顶堆来实现,然后根据大顶堆的特点,将根结点元素放到最后(这里实现升序排序),然后将剩下的元素再构成一个大顶堆,依次进行,直到堆的长度为1结束排序。
说一下什么是堆,堆是一种数据结构,首先是一个完全二叉树(有右子树必有左子树的二叉树),另外,这个完全二叉树的各个结点上的数值从根结点到每一个叶子结点都会有一种规律:
父结点一定大于或者等于其孩子结点,这样的称为大顶堆
父结点一定小于或者等于其孩子结点,这样的称为小顶堆
算法实现:
/**
*
* 堆排序:是对直接选择排序的一种优化,直接选择排序算法在每一趟比较两个数大小时, 只是比较大小没有做任何操作,
* 而堆排序针对此处做了优化,他在比较的同时也将其他的元素(不是最小的元素)做了相应调整。堆排序是先使用待排序的序列构建一个堆,这里用大顶堆来实现,
* 然后根据大顶堆的特点,将根结点元素放到最后(这里实现升序排序),然后将剩下的元素再构成一个大顶堆,依次进行,直到堆的长度为1结束排序。
* 存储堆时的数据下标在1开始,而不是0,因为可以直接使用二叉树的性质进行堆的构建
* @author chaizepeng
*/
private static void heapSort(int[] array) {
//这里已经-1了
int len = array.length-1;
//用待排序的序列构建一个大顶堆,因为排序是借助堆结构进行的,这里相当于初始化堆
//存储序列的数组下标必须在1开始,根据平衡二叉树的顺序存储特性可以知道,len/2之后的是叶子节点,而len/2以及它前边的结点是存在子结点的
//依次遍历每一个存在子结点的结点,然后进行判断、交换结点,使得构建一个堆
//1、初始化最大堆
for (int i = len/2; i > 0; i--) {
heapAdjust(array, i, len);
}
for (int i = 1; i < array.length; i++) {
System.out.print(array[i]+" ");
}
System.out.println("--------------------------");
//2、交换根结点和最后一个结点位置,将剩下的重新构建一个堆
for (int i = len; i > 1; i--) {
//交换元素
int temp = array[i];
array[i] = array[1];
array[1] = temp;
heapAdjust(array, 1, i-1);
}
}
/**
* 构建大顶堆
* 什么是堆:
* 堆是一种数据结构,首先是一个完全二叉树(有右子树必有左子树的二叉树),另外,这个完全二叉树的各个结点上的数值从根结点到每一个叶子结点都会有一种规律:
* 父结点一定大于或者等于其孩子结点,这样的称为大顶堆
* 父结点一定小于或者等于其孩子结点,这样的称为小顶堆
* @author chaizepeng
*/
private static void heapAdjust(int[] array, int i, int len) {
//遍历当前操作结点对应的子结点
int j ;
for (j = i * 2; j <= len ; j *= 2) {
//记录一下当前操作的结点
int temp = array[i];
//子结点的左孩子和右孩子进行比较
//这里为什么要比较一下呢?假设子结点比父节点大,则需要上浮子结点,但是有可能有左右两个结点,这里比较一下,确定哪一个结点上浮
//此处j < len 必须要加
if (j < len && array[j] < array[j+1]) {
++j;
}
//如果当前操作的结点 > 子结点 ,不操作
if (temp >= array[j]) {
break;
}
//交换父子结点的值
array[i] = array[j];
array[j] = temp;
//将需要判断的元素下标改成下降的元素下标,用于与其子节点进行判断
i = j;
}
}
算法分析:
堆排序的时间复杂度是O(n㏒n),算法是不稳定的。堆排序算法充分利用了完全二叉树的性质,效率高,比较复杂,不容易理解,比较适合元素多的序列排序使用。
6、归并排序
算法描述:归并排序使用的是算法设计中分治的思想,分而治之,然后合并,将小的子序列进行排序,然后慢慢的将有序的子序列进行合并,最终使得整个序列有序,这里介绍的是二路归并算法,也就是每次只将两个子序列进行归并。具体操作是这样的,每次是将两个序列A、B进行合并,这里假设这两个序列是有序的,首先初始一个长度为两个序列长度之和的容器,然后声明两个标记位i,j,i指向序列A的第一个元素,j指向序列B的第一个元素,然后比较两个数组中标记位上数的大小,哪一个小就将标记位上的数放到初始的容器中,然后将标记位指向下一个元素,然后直至其中一个序列中的元素已被移动完毕,则将另一个序列中的元素复制到容器中,排序完毕,这只是核心的两个序列归并逻辑。
算法实现:
public static void main(String[] args) {
int []array = {24, 27 ,41, 44, 19, 47 ,50 ,5,65, 93 ,94 };
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
System.out.println();
System.out.println("-----------------------------");
//从第一个元素开始,第一次归并时,每一个归并的序列长度是1(默认每一个序列长度为1的序列是有序的)
mergeSort(array,1);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
}
/**
*
* @author chaizepeng
*
* @param array 排序总序列
* @param len 要归并的序列的长度
*/
private static void mergeSort(int[] array ,int len) {
//序列{6,5,8,4,7,9,2,1,4}要进行归并,第一次归并时需要将相邻的两个元素进行排序归并,分组如下
//6,5,8,4,7,9,2,1,4
//此时,需要比较两个相邻的元素,所以,就需要进行分组和排序
//6,5 8,4 7,9 2,1 4
//需要先判断一下要分几个组
int count = array.length / (len * 2);
//判断一下count的值,如果=0的话,则说明len*2已经大于序列总长度,这时序列已经排序完成,结束即可
if (count == 0) {
return;
}
//然后依次归并
for (int i = 0; i < count; i++) {
//i*len 第一个序列的第一个元素位置
//len 有序序列长度
//(2+i)*len 第二个序列的最后一个元素位置(不包括)
merge(array,i*len*2,(i+1)*len*2,len);
}
//在这里判断一下是否归并正好两两对应,如果有剩下的,则也需要归并一下(这里是拿剩下的序列组和它的前一组进行归并操作)
int rem = array.length % (len * 2);
if (rem != 0) {
merge(array, array.length-rem-len, array.length,len);
}
//进行完一次归并后,继续下一次操作
//子序列长度为len的已经归并完成,下一次使用len*2作为长度继续归并
mergeSort(array,len * 2);
}
/**
* 一次归并过程
* @author chaizepeng
*
* @param array
* @param leftBegin
* @param rightEnd
* @param len
*/
private static void merge(int[] array, int leftBegin, int rightEnd,int len) {
//标记位,用于复制数组用
int flag = leftBegin;
//用一个数组来存一下需要合并的序列
int []temp = new int[rightEnd - leftBegin];
int leftEnd = leftBegin + len;
int rightBegin =leftEnd;//右边序列开始的位置
//标记temp的下标
int j = 0;
//比较两个序列中的元素大小,进行填充
while (leftBegin < leftEnd && rightBegin < rightEnd) {
if (array[leftBegin] > array[rightBegin]) {
temp[j++] = array[rightBegin++];
}else {
temp[j++] = array[leftBegin++];
}
}
//判断一下前后两个被比较的序列那个还有元素剩余,则直接复制到temp中
while(leftBegin < leftEnd) {
temp[j++] = array[leftBegin++];
}
while(rightBegin < rightEnd) {
temp[j++] = array[rightBegin++];
}
//将temp中的数据填到array中
for (int k = 0; k < temp.length; k++) {
array[flag+k] = temp[k];
}
}
算法分析:
归并算法的时间复杂度是O(n㏒n),算法是稳定的,效率较高。
7、性能比较
没有绝对好的算法,要根据具体的情况来分析那个算法更好,平均情况下快排(个人比较喜欢快排)、堆排序和归并排序效率高;如果排序的序列基本有序,那么冒泡排序和直接插入排序效率比较高;如果序列基本逆序,则堆排序和归并排序效率高;在空间复杂度上看,堆排序是最好的。但是快排是最常用的。
附加一张算法指标对比表: