数据结构—常见的排序算法总结

目录

  1. 插入排序 2
    1.1 直接插入排序 2
    1.1.1 算法原理 2
    1.1.2 算法流程 3
    1.1.3 算法实现 3
    1.2 折半插入排序 5
    1.2.1 算法原理 5
    1.2.2 算法代码实现 7
    1.3 希尔排序 8
    1.3.1 算法原理 8
    1.3.2 代码实现 10
    1.4 三种插入排序总结 11
    1.4.1 直接插入排序 11
    1.4.2 折半插入排序 11
    1.4.3 希尔(sehell)排序 12
  2. 选择排序 12
    2.1 简单选择排序 12
    2.1.1 算法原理和流程 12
    2.1.2 算法实现 13
    2.2 堆排序 14
    2.2.0 预备知识 14
    2.2.1 算法原理 14
    2.2.2 代码实现 17
    2.3 选择排序总结 19
    2.3.1 直接(简单)选择排序 19
    2.3.2堆排序 19
  3. 交换排序 20
    3.1 冒泡排序 20
    3.1.1 算法原理 20
    3.1.2 代码实现 20
    3.2 快速排序 21
    3.2.1 算法原理 22
    3.2.2 代码实现 25
    3.3交换排序总结 26
    3.3.1 冒泡排序 26
    3.3.2 快速排序 27
  4. 归并排序 27
    4.1 算法原理 27
    合并相邻有序子序列:我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。 28
    4.2 代码实现 30
    4.2 归并排序总结 32
    1、归并排序算法的性能 32
    2、时间复杂度 32
    3、空间复杂度 32
    4、算法稳定性 32
    5、归并排序和堆排序、快速排序的比较 32
  5. 基数排序 33
    5.1 算法原理 33
    5.2 代码实现 34

1.插入排序
什么是插入排序?
将一个记录插入到已排好序的序列中,从而得到一个新的有序序列(将序列的第一个数据看成是一个有序的子序列,然后从第二个记录逐个向该有序的子序列进行有序的插入,直至整个序列有序),既然就是不断的依次将元素插入前面已排好序的序列中。
重点:使用哨兵,用于临时存储和判断数组边界。
1.1直接插入排序
1.1.1算法原理
直接插入排序的思想就是当发现序列不是有序的情况下时,将待排序元素插入到前面已经有序的序列中。所以怎么插是核心问题,其实归根结底还是找到待排序元素的插入位置。由于是从小到大排序,可由待排序元素的前一个位置开始从后向前比较,若发现比较元素大于待排序元素,则将比较元素向后移位以腾出位置。最终将元素赋值给排序结束腾出的位置。
  边比较边移位是其特点,所以有待优化的地方。后续的折半插入和希尔排序都是对直接插入排序的一种优化。时间复杂度为o(n*n)。

1.1.2算法流程

1.1.3算法实现
package com.yzm.insertSort;

/**

  • 直接插入排序
  • @author yzm

/
public class DirectInsertionSort {
/
*
* 方法directInsertionSort()的代码优化
* (优化的目的是使代码更加简洁,读起来更直观,有利于读代码时把重心放在算法思想上)
*/
public static int[] directInsertionSort2(int[] arr) {
if(arr == null || arr.length < 2) {
return arr;
}
for(int i = 1;i < arr.length;i++) {
for(int j = i;j > 0 && arr[j] < arr[j - 1];j–) {
//arr[j-1]这个元素前移
swap(arr,j,j - 1);
}
}
return arr;
}
private static void swap(int[] arr, int j, int i) {
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}

public static int[] directInsertionSort(int[] arr) {
	int temp;// 使用哨兵,用于临时存储和判断数组边界。
	// 开始循环,循环位置从第2个元素开始,初始时默认第一个元素为一个有序序列,从第二个开始判断是否插入,一共循环(n-1)次,n为数组长度
	for (int i = 1; i < arr.length; i++) {
		if (arr[i] < arr[i - 1]) {
			temp = arr[i];
			// 若待排元素往前进行比较
			for (int j = i; j >= 0; j--) {
				//j小于0也可以直接插入了,因俄日这个时候待排元素比有序序列中所有元素都要小,这一届插入到第一个位置就可以了。
				if (j > 0 && arr[j - 1] > temp) {
					// 元素(j-1)后移以为
					arr[j] = arr[j - 1];
				} else {
					//当j循环走到-1时,直接在有序序列最开始的位置插入待排元素(哨兵)
					arr[j] = temp;
					// 结束内循环,回到外循环,也可以不写这句代码,因为前面都是有序的,继续循环
					break;
				}
			}
		}
	}
	return arr;
}

}

1.2折半插入排序
直接插入排序的缺点:在第i-1趟插入时,需要把第i个元素插入到前面的i-1个元素中,该算法总是从i-1个元素开始逐个比较之前的每个元素,直到找到第i个元素的插入位置,这显然没有利用前面0~i-1个元素已经有序的特点
优化:在0~i-1个有序元素给第i个元素寻找插入的位置时,使用二分查找法可以有效提高查找插入位置的时间效率,经过优化的插入排序称为折半插入排序,折半插入排序的时间复杂度为O(n*logn)
1.2.1算法原理
算法原理:折半插入算法是对直接插入排序算法的改进,排序原理同直接插入算法。
折半插入排序与直接插入排序算法原理相同。只是,在向已排序的数据中插入数据时,采用来折半查找(二分查找)。先取已经排序的序列的中间元素,与待插入的数据进行比较,如果中间元素的值大于待插入的数据,那么待插入的数据属于数组的前半部分,否则属于后半部分。依次类推,不断缩小范围,确定要插入的位置。
直接插入排序中,总是边比较边移动元素。下面将比较和移动查找分离开来,即先折半查找出元素的待插入位置,然后再统一的移动待插入位置之后的所有元素。因为前面的元素已经是有序的,所以可以用折半查找找出待排元素应该插入的位置。
区别:在插入到已排序的数据时采用来折半查找(二分查找),取已经排好序的数组的中间元素,与插入的数据进行比较,如果比插入的数据大,那么插入的数据肯定属于前半部分,否则属于后半部分,依次不断缩小范围,确定要插入的位置。
图解:

1.2.2算法代码实现
package com.yzm.insertSort;

/**

  • 希尔排序
  • @author yzm

*/
public class ShellInsertSort {

public static int[] shellSort(int[] arr) {
	if (arr == null || arr.length < 2)
		return arr;
	int gap = arr.length;// 增量,最开始为数组长度(只有一个子序列)
	int j = 0;//在这儿声明单纯是为了能在最内层循环外还能使用它
	// 桌布缩小增量
	for (gap = 4; gap > 0; gap /= 2) {
		// 从第gap个元素,逐个对其所在组进行直接插入排序操作
		for (int i = gap; i < arr.length; i++) {
			// 挖坑
			int temp = arr[i];
			for (j = i - gap; j >= 0 && temp < arr[j]; j -= gap) {
				/**
				 * 查找了很多资料,个人认为采用交换法移动元素不好,
				 *   因为那样的话被待排元素多次赋值给数组相应的位置上,实际上原始的算
                 *   法里被排元素只多次比较,但只有一次赋值
				 */
				//移动元素
				arr[j + gap] = arr[j];
				// 插入排序采用交换法

// swap(arr, j, j + gap);
}
//填坑
arr[j + gap] = temp;
}

	}
	return arr;
}
//交换元素(不推荐这一种一定元素的方式)
private static void swap(int[] arr, int j, int i) {

	int temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
}

}

1.3希尔排序
1.3.1算法原理
希尔排序,又称“缩小增量排序”,也是插入排序的一种,希尔排序在时间效率上其他排序有很大的改进。

原理:希尔排序的基本思想是:先将待排序表分割成若干个子表,分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。

流程:将待排序的一组元素按一定的间隔分为若干个序列,分别进行直接插入排序。
开始时设置的间隔比较大,在每轮排序中将间隔逐步减小,直到间隔为1,
也就是最后一步是简单(直接)插入排序。
通过此种方式,对于关键字的值较小的记录,其前移的过程不是一步一步的,而是跳跃性的前移,并且在最后一次对整表进行插入排序时减少了比较和排序的次数。
一般在记录的数量多的情况下,希尔排序的排序效率较直接插入排序高
图解:

可以看到,较小间隔的排序仍然保持着较大间隔的性质,即更小间隔的排序没有把上一步的结果变坏
希尔排序的过程中,对于分割的每个子表,其各自包含的记录在原表中并不是相互挨着的,而是相互之间相隔着某个固定的常数。例如上例中第一次排序时子表中的记录分割的常量为 5,第二次排序时为 3。
通过此种方式,对于关键字的值较小的记录,其前移的过程不是一步一步的,而是跳跃性的前移,并且在最后一次对整表进行插入排序时减少了比较和排序的次数。
Tips:一般在记录的数量多的情况下,希尔排序的排序效率较直接插入排序高。
1.3.2代码实现
package com.yzm.insertSort;

/**

  • 希尔排序
  • @author yzm

*/
public class ShellInsertSort {

public static int[] shellSort(int[] arr) {
	if (arr == null || arr.length < 2)
		return arr;
	int gap;// 增量
	// 桌布缩小增量
	for (gap = 4; gap > 0; gap /= 2) {
		// 从第gap个元素,逐个对其所在组进行直接插入排序操作
		for (int i = gap; i < arr.length; i++) {
			// 挖坑
			int temp = arr[i];
			for (int j = i - gap; j >= 0 && temp < arr[j]; j -= gap) {
				// 插入排序采用交换法
				swap(arr, j, j + gap);
			}
		}
			
	}
	return arr;
}

//交换两个元素
private static void swap(int[] arr, int j, int i) {

	int temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
}

}

Tips:提示:所选取的增量值最好是没有除 1 之外的公因子(纯数学问题),同时整个增量数组中最后一个增量值必须等于 1 ,因为最后必须对整张表做一次直接插入排序算法。

1.4三种插入排序总结
1.4.1直接插入排序
空间效率:仅使用了常数个辅助空间,因而空间复杂度为O(1)
时间效率:在排序过程中,向有序子表中逐个的插入元素的操作进行了 n-1趟,每趟操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。

时间复杂度:

1.在最好情况下,严格递增的数组,表中的元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素。比较次数C和移动次数M为:
C = n - 1
M = 0
时间复杂度为:O(n)。
2.在最好情况下,严格递减的数组(逆序),总的比较次数达到最大 ,总的移动数也达到最大比较次数C和移动次数M为:
C = n(n-1)/2
M = n(n-1)/2
时间复杂度为:O(n2)。
3.平均情况下,考虑待排序的表中的元素是随机的,此时可以取上述最好与最坏的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数约为n^(2)/4。

由此,直接插入排序的时间复杂度为O(n(2)),虽然折半查找排序算法的时间复杂度也有O(n(2)),但对于数据量比较小的排序表,折半插入排序往往能表现出很好的性能。

稳定性:直接插入排序是一个稳定的插入排序。适用于顺序排序存储和链式存储的线性表,当为链式存储时,可以从前往后查找指定元素的位置。注意:大部分排序算法都仅适用于顺序存储的线性表。

1.4.2折半插入排序
折半插入排序仅仅是减少了比较元素的个数,约为O(Nlog2N),该比较次数与待排序表的初始状态无关,仅仅取决于表中的元素个数n;而元素移动的次数并没有改变(元素移动次数跟直接插入一样),他依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为O(n2)。折半插入排序是一个稳定的排序方法。

1.4.3希尔(sehell)排序
直接插入排序适用于基本有序的排序表和数据量不大的排序表。基于这两点,提出了希尔排序,又称为缩小增量排序

空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1);

希尔排序是不稳定的排序算法,仅适用于当线性表为顺序存储的情况。

时间效率:由于希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当n在某个特定范围内,希尔排序的时间复杂度约为O( n(1.3)),在最坏的情况下时间复杂度是O(n(2)。

2.选择排序
2.1简单选择排序
2.1.1算法原理和流程
原理:
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置(也可以放在最后),然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾(或者最前面)。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
大白话就是:第一趟选一个最大的(最小的)元素,放在序列起始位置或末尾,第二趟选一个次大(小)的,放在第二个位置(或倒数第二个位置),直达所有的元素都有序。

流程:
(略:真的太简单了)
算了,还是写一写吧:
1.首先从原始数组中选择最小的一个数据,将其和位于第一个位置的数据进行交换。
2.接着从剩下的 n-1 个数据中选择次小的一个元素,将其和第二个位置的数据交换。
3. 然后,这样不断重复,直到最后连个数据完成交换。至此,便完成对原始数组的从小到大的排序。

流程图解:
(略:这个真的是省略了)
2.1.2算法实现
package com.yzm.selectSort;

/**

  • 简单选择排序
  • @author yzm

*/
public class SelectSort {
public static int[] selectSort(int[] arr) {
if(arr == null || arr.length <= 1) {
return null;
}
for(int i = 0;i < arr.length;i++) {
//记录最小元素的位置
int min = i;
//第 i =次循环选出一个最小的树
for(int j = i + 1;j < arr.length;j++) {
if(arr[j] < arr[min]) {
min = j;
}
}
if (min != i ) {
//该最小元素与第i个元素交换位置
swap(arr,min,i);
}
}
return arr;
}

private static void swap(int[] arr, int j, int i) {
	int temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
}

}

2.2堆排序
2.2.0 预备知识
大顶堆和小顶堆:堆,是一种特殊的二叉树(完全二叉树),每个子结点的值总是小于(或者大于)它的父结点,相应的分为最大堆和最小堆。

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
(i为结点编号,从0开始)

完全树的一个重要性质:
若以0开始编号,对编号为 i 的结点,其左孩子为2i+1,右孩子为2i+2,二者相差为1
第一个非叶子结点编号为(int)(length/2)-1
最后一个结点的编号为length-1
若以1开始编号,对编号为 i 的结点,其左孩子为2i,右孩子为2i+1,二者相差为1
第一个非叶子结点编号为(int)((length - 1)/2)
最后一个结点的编号为length

2.2.1算法原理
基本原理:利用最大堆(对应升序)或者最小堆(对应降序)输出堆顶元素,即最大值(或最小值),
然后将剩下元素重新生成最大堆(或者最小堆,继续输出堆顶元素,重复此过程直到全部元素都已输出即可得到有序序列
1.构造初始堆(大顶堆或小顶堆):
对初始序列建堆,就是一个反复筛选的过程:n 个结点的完全二叉树,最后一个结点是第 n/2 (编号从一开始)个结点的孩子。对第 n/2 个结点为根的子树进行筛选(对于大根堆:若根结点的关键字小于左右子女结点中关键字较大者,则交换),使该子树成为堆。之后向前依次对以各结点为根的子树进行筛选,看该结点值是否大于其左右子节点的值,若不是,将左右子节点中较大值与之交换,交换之后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该节点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根节点。
2.排序:每次交换第一个元素跟最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。

算法流程:
1.先将一个无序的序列生成一个最大堆
2.将堆顶元素与堆的最后一个元素对换位置
3.将剩余的元素重新生成一个最大堆
4.重复2-3步骤,直到堆中只剩一个元素

两个示例:

1.构建初始堆

2.排序(就是交换)
注:建堆和排序所用的的数据不一样,理解了原理就好

2.2.2代码实现
package com.yzm.selectSort;

import java.util.Arrays;

/**

  • 堆排序
  • @author yzm

*/
public class HeapSort {
public static void heapSaort(int[] arr) {
if(arr == null || arr.length < 2) {
return;
}
System.out.println("建堆后: ");
int[] buildMaxHeap = buildMaxHeap(arr);
System.out.println(Arrays.toString(buildMaxHeap));
System.out.println(“排序输出”);
arrShow(buildMaxHeap);

}
//构建大顶堆
private static int[] buildMaxHeap(int[] arr) {
	int len = arr.length;
	//根据输入数组建立大顶堆
		//如果编号从0开始:n个结点完全二叉树最后一个非叶子结点为(n/2 - 1)
	for(int i = len/2 -1;i >= 0;i--) {
		adjustHeap(arr,i,len);
	}
	return arr;
}

//调整下沉(根节点)
private static void adjustHeap(int[] arr,int current,int len) {
	//取出当前根节点的值
	int temp = arr[current];
	for(int k = 2*current + 1; k < len;k = 2*current + 1) {
		if(k < len - 1 && arr[k] < arr[k+1]) {
			k++;
		}
		if(temp >= arr[k]) {
			//如果满足这个条件,直接结束整个循环,因为结点k的孩子肯定是大顶堆
			break;
		}else {
			swap(arr,current,k);
			//移动一下指针,指向比较结果更大的那个孩子
			current = k;
		}
	}
}
//排序输出,输出为为一个有序序列
private static void arrShow(int[] arr) {
	for(int i = arr.length -1;i >= 0;i--) {
		//将堆顶元素与堆的最后一个元素对换位置
		swap(arr,0,i);
		//剩下的元素重新生成一个大顶堆
		adjustHeap(arr, 0, i);
	}
	System.out.println(Arrays.toString(arr));
}
private static void swap(int[] arr, int j, int i) {
	int temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
}

}

2.3 选择排序总结
2.3.1 直接(简单)选择排序
空间效率:O(1)

时间效率:
元素移动次数很少,当表有序时移动次数为0,但比较的次数与表的次序无关,所以时间复杂度始 终为O(n*n)

不稳定的算法:
比如:排序前:
2,4,4*,3
排序后:
2,3,4*,4
2.3.2堆排序
空间效率:
不需要额外的辅助数组,空间复杂度为O(1)

时间效率:
时间复杂度:

稳定性:不稳定

特点:
所以总体来说,堆排序的时间复杂度为O(NlogN)
它对原始记录的排序状态并不敏感,最好最坏和平均时间复杂度都是O(NlogN)
在空间上,只有一个用来交换的暂存单元,空间复杂度也很好
由于记录的比较与交换是跳跃式进行,因此也是不稳定的。

tips:由于初始构建所需的比较次数较多,因此不适合待排序序列较少的情况

3.交换排序
3.1冒泡排序
3.1.1算法原理
原理:
1.通过相邻两个元素之间的比较和交换,使较大的元素(或者较小的元素)逐渐从前面移向后面(升序),就像水底下的气泡一样逐渐向上冒泡,所以被称为“冒泡”排序。冒泡排序的最坏时间复杂度为O(n2),平均时间复杂度为O(n2)
2.重复的遍历要排序的数组,每次遍历过程中从头至尾比较两个相邻的元素,若顺序错误则交换两个元素。
3.每一趟只能确定将一个数归位,如果有n个数进行排序,只需将n-1个数归位,也就是说要进行n-1趟操作,而每一趟都需要从第1位开始进行相邻两个数的比较。

流程:
(略)

示意图:

(略)
3.1.2代码实现
package com.yzm.exchangeSort;

public class BubbleSort {

/**
 * 冒泡排序的优化
 *   	优化原理:当第i次冒泡排序一次都没有交换,说明该序列已经变成有序的了,
 *   	这样就减少了比较次数(注意是比较次数,不是交换次数)
 */
public static int[] bubleSort2(int[] arr) {
	int count = 0;
	for(int i = 0;i <= arr.length - 1;i++) { //n个数排序只需要n-1趟
		int flag = 0;//标志
		for(int j = 0;j < arr.length-1-i;j++) { //每一趟比较到n-i-1结束
			count ++;
			if(arr[j] > arr[j+1]) {
				swap(arr,j,j+1);
				flag = 1;
			}
		}
		if(flag == 0) { /* 一次交换都没有,说明已经是有序序列,直接结束方法*/
			System.out.println("bubleSort2的比较次数: " + count);
			return arr;
		}
	}
	System.out.println("bubleSort2的比较次数: " + count);
	return arr;
}

public static int[] bubleSort(int[] arr) {
	int count = 0;
	for(int i = 0;i <= arr.length - 1;i++) {
		//不明白为什么这么写得不到正确结果

// for(int j = 0;(j < (arr.length-1-i)) && (arr[j] > arr[j+1]) ;j++) {
// swap(arr,j,j+1);
// }
for(int j = 0;j < arr.length-1-i;j++) {
count ++;
if(arr[j] > arr[j+1]) swap(arr,j,j+1);
}
}
System.out.println("bubleSort的比较次数: " + count);
return arr;
}

private static void swap(int[] arr, int j, int i) {
	int temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
}

}

3.2快速排序

快速排序(Quick Sort) 是基于“分治”对冒泡排序的一种改进方法,在冒泡排序中,进行元素的比较和交换是在相邻元素之间进行的,元素每次交换只能移动一个位置,所以比较次数和移动次数较多,效率相对较低。而在快速排序中,元素的比较和交换是从两端向中间进行的,较大的元素一轮就能够交换到后面的位置,而较小的元素一轮就能交换到前面的位置,元素每次移动的距离较远,所以比较次数和移动次数较少,速度较快,故称为“快速排序”。

3.2.1算法原理
原理:

通过一轮排序将待排序元素分割成独立的两部分, 其中一部分的所有元素均比另一部分的所有元素小,然后分别对这两部分的元素继续进行快速排序(递归),以此达到整个序列变成有序序列。快速排序的最坏时间复杂度为O(n2),平均时间复杂度为O(nlog2n)
快速排序在最坏的情况下,仍可能是相邻的两个数进行交换,因此快速排序最差时间复杂度和冒泡排序是一样的,都是O(N2),它的平均时间复杂度为O(n
log2n)

流程:
第一步:设置两个指针left和right分别指向数组的头部和尾部,并且以头部的元素(6)为基准数
第二步:right指针先往左移动,找到小于基准数的元素就停下,然后移动left指针(想一下为什么是right先移动,不能是left先移动)
第三步:left指针往左移动,找到大于基准数的元素就停下,然后交换right和left指针所值元素的值
重复第二、三步,直到两个指针left和right重合
第四步:两个指针重合后将基准数(6)与两个指针指向的元素值

示意图:

简洁一点吧:

3.2.2代码实现

package com.yzm.exchangeSort;

/**

  • 快速排序
  • @author yzm

*/
public class QuickSort {

public static int[] quickSout(int[] arr) {
	int low = 0;
	int high = arr.length - 1;
	quickSout(arr,low,high);
	return arr ;
}

private static void quickSout(int[] arr, int low, int high) { //递归何时结束?
	if(low < high) { //递归结束条件:左右指针重合
		//分区操作,将一个数组分成两个分区,返回分区界限索引
		int index = separate(arr, low, high);
		//对做左分区进行快排
		quickSout(arr, low, index - 1);
		//对右边进行快排
		quickSout(arr, index + 1, high);
	}
}

private static int 	separate(int[] arr, int i, int j) {
	// 将第一个数作为基准值,挖坑
	int mid = arr[i];
	while (i < j) {
		// 使用循环实现分区操作
		while (i < j && arr[j] >= mid) {
			// 右指针左移动,找到第一个小于基准值的数
			j--;
		}
		// 将右侧找到的小于基准值的数移到左边(坑)的位置
		if (i < j) {
			arr[i] = arr[j];
			i++; // 填号坑后,左指针后移
		}

		while (i < j && arr[i] <= mid) {
			i++;
		}
		if (i < j) {
			// 将左侧找到的大于基准值的数移到左边(坑)的位置
			arr[j] = arr[i];
			j--; // 填号坑后,又指针后移
		}
	}
	//使用基准值填坑,这就是基准值的最终位置
	arr[i] = mid;
	return i;
}

}

3.3交换排序总结

3.3.1 冒泡排序
空间效率:

从实现原理可知,冒泡排序是在原输入数组上进行比较交换的(称“就地排序”),所需辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)

时间效率:
冒泡排序耗时的操作有:比较 + 交换(每次交换两次赋值)。时间复杂度如下:
1)        最好情况:序列是升序排列,在这种情况下,需要进行的比较操作为(n-1)次。交换操作为0次。即O(n)
2)        最坏情况:序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。交换操作数和比较操作数一样。即O(n^2)
3)        渐进时间复杂度(平均时间复杂度):O(n^2)

稳定性:

冒泡排序是稳定的,不会改变相同元素的相对顺序。

3.3.2 快速排序
空间效率:

从实现原理可知,快速排序是在原输入数组上进行比较分区的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)

时间效率:

快速排序耗时的操作有:比较 + 交换(每次交换两次赋值)。时间复杂度如下:
1)最好情况:选择的基准值刚好是中间值,分区后两分区包含元素个数接近相等。因为,总共经过x次分区,根据2^x<=n得出x=log2n,每次分区需用n-1个元素与基准比较。所以O(nlog2n)

2) 最坏情况:每次分区后,只有一个分区包含除基准元素之外的元素。这样就和冒泡排序一样慢,需n(n-1)/2次比较。即O(n^2)
3) 渐进时间复杂度(平均时间复杂度):O(nlog2n)

快速排序在最坏的情况下,仍可能是相邻的两个数进行交换,因此快速排序最差时间复杂度和冒泡排序是一样的,都是O(N2),它的平均时间复杂度为O(NlogN)

稳定性:

快速是不稳定的,会改变相同元素的相对顺序

4.归并排序
4.1算法原理
基本思想:归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

示意图:

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
合并相邻有序子序列:我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

4.2 代码实现

package com.yzm.mergeSort;

public class MergeSort {

public static int[] mergeSort(int[] arr) {
	//排序前,先左建一个长度等于原数组的临时数组
	int[] temp = new int[arr.length];
	sort(arr,0,arr.length - 1,temp);
	return arr;
}
private static void sort(int[] arr,int low,int high,int[] temp) {
	/**
	 * 注意:这里只能使用if,一定不能用循环,因为如果用了循环,low和high的值永远不变,循环田间永远满足,
	 * 		所以就是一个死循环。
	 */
	if(low < high) {
		int mid = (low + high)/2;
		//左边归并排序,使得左子序列有序
		sort(arr,low,mid,temp);
		//右边归并排序,使得右子序列有序
		sort(arr, mid + 1, high, temp);
		//将两个有序子序列合并
		merge(arr,low,mid,high,temp);
	}
}
private static void merge(int[] arr, int low, int mid, int high, int[] temp) {
	int i = low; //左序列指针
	int j = mid + 1; //右序列指针
	int t = low; //临时数组指针	
	/**
	 * 这句代码指出了左指针和右指针的变化范围,两个指针只要一个指针超出了范围据结束循环,但是另一个子序列的指针
	 * 并没有到头啊,所以后面则增加了两个循环,如果一个指针到头了,直接填充另一个子序列即可
	 */
	while(i <= mid && j <= high) { 
		if(arr[i] <= arr[j]) {
			temp[t++] = arr[i++];
		}else {
			temp[t++] = arr[j++];
		}
	}
	//如果右指针已经走到头了,就不用比较了,直接将左子序列剩余的元素加入临时数组中
	while(i <= mid) {
		temp[t++] = arr[i++];
	}
	//如果左指针已经走到头了,直接将左子序列剩余的元素加入临时数组中
	while(j <= high) {
		temp[t++] = arr[j++];
	}
	//将临时数组中的内容全部拷贝到原始数组中
	while(low <= high) {
		t = low;
		arr[low++] = temp[t++];
	}
	
}

}

4.2归并排序总结
1、归并排序算法的性能

2、时间复杂度
归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是O(n*log2n)。
3、空间复杂度
由前面的算法说明可知,算法处理过程中,需要一个大小为n的临时存储空间用以保存合并序列。
4、算法稳定性
在归并排序中,相等的元素的顺序不会改变,所以它是稳定的算法。
5、归并排序和堆排序、快速排序的比较
若从空间复杂度来考虑:首选堆排序,其次是快速排序,最后是归并排序。
若从稳定性来考虑,应选取归并排序,因为堆排序和快速排序都是不稳定的。
若从平均情况下的排序速度考虑,应该选择快速排序。

5.基数排序
5.1算法原理
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序与本系列前面讲解的七种排序方法都不同,它不需要比较关键字的大小。它是根据关键字中各位的值,通过对排序的N个元素进行若干趟“分配”与“收集”来实现排序的。

基本思想(以整数为例):将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

相比其它排序,主要是利用比较和交换,而基数排序则是利用分配和收集两种基本操作。基数 排序是一种按记录关键字的各位值逐步进行排序的方法。此种排序一般适用于记录的关键字为整数类型的情况。所有对于字符串和文字排序不适合。
      实现:将所有待比较数值(自然数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
      基数排序的两种方式:
1.高位优先,又称为最有效键(MSD),它的比较方向是由右至左;
2.低位优先,又称为最无效键(LSD),它的比较方向是由左至右;

流程示意图:

5.2 代码实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值