八大排序算法
| 排序算法 | 平均时间复杂度 | 稳定性 |
|---|---|---|
| 冒泡排序 | O(n^2) | 稳定 |
| 选择排序 | O(n^2) | 不稳定 |
| 直接插入排序 | O(n^2) | 稳定 |
| 希尔排序 | O(n^1.5) | 不稳定 |
| 快速排序 | O(N*logN) | 不稳定 |
| 归并排序 | O(N*logN) | 稳定 |
| 堆排序 | O(N*logN) | 不稳定 |
| 基数排序 | O(d(n+r)) | 稳定 |
1.冒泡排序
基本原理:
对于存放原始数据的数组,按照从前往后的方向进行多次扫描,每次扫描称为一趟。当发现相邻的两个数据的次序与排序要求的大小次序不一致时,将这两个数据进行互换。如果从小到大排序,这时,较小的数据就会逐个向前移动,就想气泡向上漂浮一样。
图示:
过程分析:
从上面的例子中我们可以看到每轮比较的次数:
| 遍历趟数i | 每趟遍历比较次数j |
|---|---|
| 0 | 5 |
| 1 | 4 |
| 2 | 3 |
| 3 | 2 |
| 4 | 1 |
我们可以从表中看到,遍历的趟数i和每趟的比较次数j之间有着这样的关系:i+j=nums.length-1,所以我们得到了i与j。i的取值范围是:[0,nums.length-1) , j的取值范围是[i+1,nums.length-1]
代码
public static int[] bubbleSort(int[] nums) {
for (int i = 0; i < nums.length-1; i++) {//趟数
for (int j = i+1; j <= nums.length-1; j++) {//每趟比较次数
if (nums[i] > nums[j]) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
}
return nums;
}
冒泡排序的优化
改进1.0
- 原始冒泡排序存在这样的问题:比如有一组数{5,8,6,3,9,2,1,7}在进行到第六轮冒泡排序之后,整个数组中的数字已经是排好序了,但是在原始的冒泡排序中,他还会继续进行第七轮比较,很显然这是没有必要的。
- 改进:我们用一个boolean值flag来作为标记数组是否已经是有序的了,初始为true,如果数组是无序的,那么就会发生交换,发生元素交换我们就把flag的值置为false 。如果在一趟比较之后,flag的值是true,那么说明数组已经是有序的了,那么就不需要进行剩余趟数的排序了。直接跳出外层循环,返回结果。
改进后的代码
public static int[] bubbleSort(int[] nums) {
for (int i = 0; i < nums.length-1; i++) {
boolean flag = true;//有序标记,每一轮初始值都是true
for (int j = i+1; j <= nums.length-1; j++) {
if (nums[i] > nums[j]) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
//有元素交换,说明数组是无序的
flag = false;
}
}
if (flag) {//在一趟结束之后如果flag已经为true,那么就说明数组已经有序了。
break;
}
}
return nums;
}
改进2.0
- 问题描述:比如待排序数组是
{3,4,2,1,5,6,7,8},我们可以看到这组数中后面的{5,6,7,8}已经是有序的了,我们就没有必要再把他们执行冒泡排序了,因此我们可以针对这种情况进行改进。 - 改进方法:这种问题的关键点在于我们对 数列有序区 的界定。按照现有逻辑,有序区的长度 和冒泡排序的 趟数 相等,但实际上可能有序区的长度要大于轮数。例如问题描述中的例子,后面的
5,6,7,8实际已经处在有序区了。我们可以在每一趟排序之后,记录下来最后一个交换元素的位置,这个位置就是无序数列的边界,再往后的数字都是有序的了。
代码:
public static int[] bubbleSort(int[] nums) {
int lastLocation = 0;//最后一个交换元素的位置
int isSorted = nums.length -1;//有序区边界,初值为数组最大索引nums.length -1
for (int i = 0; i < nums.length-1; i++) {
boolean flag = true;//有序标记,每一轮初始值都是true
for (int j = 0; j < isSorted; j++) {//注意
if (nums[j] > nums[j+1]) {
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
//有元素交换,说明数组是无序的
flag = false;
lastLocation = j;
}
}
isSorted = lastLocation;
if (flag) {//在一趟结束之后如果flag已经为true,那么就说明数组已经有序了。
break;
}
}
return nums;
}
2.选择排序
基本原理:
选择排序是一种简单的、直观的排序方式。核心思想是:每次从待排序的数字中找出最小值,将最小值放在待排序数字的最前面,这样就确定了一个数字的有序位置。接着,在剩下的未排序的数字中,再找出一个最小值出来,放在剩下数字的前面,这样就确定了第二个数字的有序位置。以此类推,n个数字只要重复n-1趟,就可以将这n个数字按照从大到小的顺序排好。
图示:
过程分析
每一趟选择排序下来,我们需要记住最小值元素所在的index下标,然后把它对应位置上的元素放到剩余未排序的数组的最前面。我们可以看到,排序趟数i和剩余待排数组长度j之间存在如下关系:i+j=arr.length-1(i从0开始)。
排序趟数i的取值:[0,n-1),剩余待排序数组的范围j的取值是:[i+1,n-1],n表示数组长度。
代码:
public static int[] selectSort(int[] arr) {
for (int i = 0; i < arr.length -1; i++) {
int minIndex = i;//初始最小值下标
for (int j = i+1; j <= arr.length -1; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; //记录新的最小值的下标
}
}
//一趟排序之后,将最小元素交换到数组最前面。
if(minIndex != i){
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;//返回排序结果
}
3.插入排序
基本原理:
在要排序的一组数中,假定前n-1个数已经是排好序的,现在将第n个数插入到前面的n-1个有序数组中,使得这n个数刚好是有序的,如此反复循环,知道所有数字都是有序的。
图示:
过程分析
从图示我们可以看出,在初始数组中,数组有序的部分是数组的第一个元素8,然后我们依次将剩余的2,6,1,7,0插入到有序数组的相应位置,因此我们总共需要进行arr.length-1 = 5次插入操作,也就是排序趟数i的取值为:[1,arr.length-1],寻找插入位置的时候,需要与当前位置i前面的i-1个元素比较,也就是j的取值范围是:[0,i-1]。
代码
public static int[] insertSort(int[] arr) {
int i,j,temp;
for (i = 1; i <= arr.length - 1; i++) {
temp = arr[i];//当前待插入数字
for (j = i - 1; j >=0; j--) {
if (arr[j] > temp) {//如果当前待插入元素小于它的前一个元素,说明插入位置还在前面,我们把比较元素后移。
arr[j+1] = arr[j];
} else {//如果当前待插入元素的值>=当前比较位置的元素,那么说明找到了合适的插入位置,跳出循环。
break;
}
}
arr[j+1] = temp;//插入到合适位置
}
return arr;
}
4.希尔排序
基本原理:
在要排序的一组数中,根据某一个增量将数组分为若干子序列,并对子序列分别进行插入排序。然后逐渐减小增量,并重复上述操作,直至增量为1,此时数据序列基本有序,最后进行插入排序。
图解:
代码:
public static int[] shellSort(int[] arr) {
int temp,i,j;
int incre = arr.length;//初始增量
while (true) {
incre /= 2;//增量每次减半
//每一趟采用插入排序
for (i = incre; i < arr.length; i++) {
temp = arr[i];//要插入的元素
for (j = i - incre; j >=0; j -= incre) {
if (arr[j] > temp) {//如果当前待插入元素小于这组数中它的前一个元素,说明插入位置还在前面,我们把比较元素后移。
arr[j+incre] = arr[j];
}else{//如果当前待插入元素的值>=当前比较位置的元素,那么说明找到了合适的插入位置,跳出循环。
break;
}
}
arr[j+incre] = temp; //插入到最终合适位置
}
//System.out.println(Arrays.toString(arr));
if(incre == 1){
break;//跳出循环
}
}
return arr;
}
5.快速排序
基本原理:分治
- 先从数列中取出一个数作为key值;
- 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
- 对左右两个小数列重复第二步,直至各区间只有1个数。
图解:
代码:
/**快速排序 */
public static void quickSort(int[] arr,int left,int right) {
if(left<right){
int mid = getLastLocation(arr, left, right);
quickSort(arr, left, mid-1);//pivot左边部分执行同样的操作
quickSort(arr, mid+1, right);//pivot右边部分执行同样的操作
}
}
/**
* 寻找pivot最终所在位置
*/
public static int getLastLocation(int[] arr,int left,int right) {
int pivot = arr[left];//初始基准值为数组第一个元素
while (left < right){
//从右往左找比pivot小的值
while (left < right && arr[right] >= pivot) {//遇到>=pivot的值,
right--;//左移right指针
}
//否则right指针指向的是<pivot的值
arr[left] = arr[right];//将它放到pivot左边。
//左右往左找比pivot大的值
while (left < right && arr[left] <= pivot) {//遇到<=pivot的值
left++;//右移left指针
}
//否则left指向的是>pivot的值
arr[right] = arr[left];//将它放到pivot右边
}
arr[left] = pivot;//将档期那pivot放到找到的最终位置上
System.out.println(pivot + "最终位置:"+ left);
return left;//返回当前pivot最终位置。
}
6.归并排序
基本思想:
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而**治(conquer)**的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
图示:
分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],具体实现步骤如下图:
代码实现:
import java.util.Arrays;
public class MergeSort{
public static void main(String[] args){
int[] arr = {8,4,5,7,1,3,6,2};
mergeSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
/**归并排序 */
public static void mergeSort(int[] arr, int left, int right){
if (left<right) {
int mid = left + (right - left) / 2;
mergeSort(arr,left,mid);
mergeSort(arr,mid + 1, right);
merge(arr,left,mid,right);
}
}
/**合并连个有序子序列 */
public static void merge(int[] arr,int left,int mid,int right){
int[] temp = new int[arr.length];//临时数组
int l = left;//左序列指针
int r = mid+1; //右序列指针
int t = 0;//临时数组指针
while(l<=mid && r<=right){//按照大小顺序插入到temp数组中。
if (arr[l] <= arr[r]) {//左序列小
temp[t] = arr[l];
t++;
l++;
}else{//右序列小
temp[t] = arr[r];
t++;
r++;
}
}
while(l<=mid){//左序列插入完了,左序列还有剩余,将剩余元素插入到temp中
temp[t] = arr[l];
t++;
l++;
}
while(r<=right){//左序列合并完了,右序列有剩余,将剩余元素插入到temp中
temp[t] = arr[r];
t++;
r++;
}
t = 0;//重置
while(left <= right){//将temp复制到arr中。
arr[left++] = temp[t++];
}
}
}
7.堆排序
基本原理
- 把无序数组构建成二叉堆。需要从小到大排序的,构建大根堆。需要从大到小的则构建小根堆。
- 循环删除堆顶元素,替换到二叉堆的末尾。调整产生新的堆顶。
图示:
如上图所示,在删除节点10的堆顶点之后,经过调整,值为9的新节点会顶替上来,成为新的堆顶,依次类推,在删除9之后,经过调整,8成为新的堆顶......
由于二叉堆的这个特性,每一次删除旧堆顶,调整后的新堆顶都是大小仅次于旧堆顶的节点。那么只要反复删除堆顶,反复调整二叉堆,所得到的集合就会成为一个有序集合,过程如下。 删除节点9,节点8成为新堆顶。
删除节点8,7成为新的堆顶:
删除节点7,6成为新的堆顶:
依次进行下去,最后原本的大根堆已经变成了一个从小到大的有序集合。二叉堆实际存储在数组中,数组中的元素排列如下。
代码:
import java.util.Arrays;
/**
* 堆排序
* 1. 把无序数组构建成二叉堆。需要从小到大排序的,构建大根堆。需要从大到小的则构建小根堆。
* 2. 循环删除堆顶元素,替换到二叉堆的末尾。调整产生新的堆顶。
*/
public class HeapSort {
public static void main(String[] args) {
int[] arr = {2,3,8,1,4,9,10,7,16,14};
System.out.println(Arrays.toString(arr));
heapSort(arr, arr.length);
System.out.println(Arrays.toString(arr));
}
/**
* 维护堆的性质
* @param arr数组
* @param n 数组长度
* @param index待维护节点的下标
*/
public static void heapify(int[] arr,int n,int index) {
int largest = index;
int lson = index * 2 +1;//当前父节点的左孩子
int rson = index * 2 +2;//当前父节点的右孩子
if (lson < n && arr[largest] < arr[lson]) {//有左孩子并且左孩子比父节点大
largest = lson;//更新最大节点下标
}
if (rson < n && arr[largest] < arr[rson]) {//有右孩子并且右孩子比父节点大
largest = rson;//更新最大节点下标
}
if (largest != index) {//将三者中最大的值,交换到父节点位置。
int temp = arr[index];
arr[index] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest);//递归
}
}
/**
* 堆排序
* @param arr
* @param n
*/
public static void heapSort(int[] arr,int n) {
//1.构建大根堆
//int i;
for (int i = (arr.length - 2) / 2; i >=0; i--) {
heapify(arr, n, i);
}
//2. 进行排序
//循环交换堆顶和最后一个节点的元素,这样最大值就放到了数组的最后面
for (int i = n-1; i > 0; i--) {
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
//调整堆
heapify(arr, i, 0);//维护堆顶元素
}
}
}
8.基数排序
图解基本原理:
代码:
/**基数排序 */
public static void radixSort(int[] arr) {
//获取数组中的最大数是几位数
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max){
max = arr[i];
}
}
//得到最大数的位数
int maxLength = (max + "").length();
//定义一个二维数组表示桶,每个桶就是一个一维数组
//为了防止放入桶中的数溢出,每个桶的大小设置维arr数组的长度
//空间换时间
int[][] bucket = new int[10][arr.length];
//为了记录每个桶中放入了多少个数据,定义一个一位数组来记录每个桶中放入的数据数目
int[] bucketElementCount = new int[10];
for (int i = 0,n = 1; i < maxLength; i++, n*=10) {
//针对每个数的数位进行排序,第一次是个位,第二次是十位...
for (int j = 0; j < arr.length; j++) {
//取出每个元素的n位的值,n=1表示个位
int digitOfElement = arr[j] / n % 10;
//放入到对应的桶中
bucket[digitOfElement][bucketElementCount[digitOfElement]] = arr[j];
bucketElementCount[digitOfElement]++;
}
//按照桶的顺序,依次取出数据,放入原来的数组
int index = 0;
//遍历灭一个桶,并将桶中的数据,放入到原数组
for (int k = 0; k < bucketElementCount.length; k++) {
//如果桶中有数据,才放入原数组
if (bucketElementCount[k] != 0) {
//循环该桶
for (int l = 0; l < bucketElementCount[k]; l++) {
//取出元素放入到arr
arr[index++] = bucket[k][l];
}
}
//第i+1轮处理后,需要讲每个bucketElementCount[k] = 0
bucketElementCount[k] = 0;
}
System.out.println("第"+(i+1) + "轮,排序处理:" + Arrays.toString(arr));
}
}