八大排序算法

856 阅读8分钟

八大排序算法

排序算法平均时间复杂度稳定性
冒泡排序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
05
14
23
32
41

我们可以从表中看到,遍历的趟数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

  1. 原始冒泡排序存在这样的问题:比如有一组数{5,8,6,3,9,2,1,7}在进行到第六轮冒泡排序之后,整个数组中的数字已经是排好序了,但是在原始的冒泡排序中,他还会继续进行第七轮比较,很显然这是没有必要的。
  2. 改进:我们用一个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

  1. 问题描述:比如待排序数组是{3,4,2,1,5,6,7,8},我们可以看到这组数中后面的{5,6,7,8}已经是有序的了,我们就没有必要再把他们执行冒泡排序了,因此我们可以针对这种情况进行改进。
  2. 改进方法:这种问题的关键点在于我们对 数列有序区 的界定。按照现有逻辑,有序区的长度 和冒泡排序的 趟数 相等,但实际上可能有序区的长度要大于轮数。例如问题描述中的例子,后面的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.堆排序

基本原理

  1. 把无序数组构建成二叉堆。需要从小到大排序的,构建大根堆。需要从大到小的则构建小根堆。
  2. 循环删除堆顶元素,替换到二叉堆的末尾。调整产生新的堆顶。

图示:

如上图所示,在删除节点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));
        }
    }