Java常见的排序算法详解

21 篇文章 3 订阅
13 篇文章 2 订阅


两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。

一、直接插入排序

整个区间被分为

  1. 有序区间
  2. 无序区间

每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入。

   public static void insertSort(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            int tmp = arr[i];
            int j = i - 1;
            for ( ; j >= 0 ; j--) {
                if (arr[j] > tmp){
                    arr[j + 1] = arr[j];
                }else{
                    //arr[j + 1] = tmp;//只要j回退的时候,遇到了比tmp小的元素,就结束了这次的比较
                    break;
                }
            }
            //j回退到了小于0的地方
            arr[j + 1] = tmp;
        }
    }
    public static void main(String[] args) {
        int[] array = {6,10,9,3,5};
        insertSort(array);
        System.out.println(Arrays.toString(array));
    }
}

输出结果:
在这里插入图片描述
时间复杂度:最好O(N)–>数据本身是有序的;
* 最坏O(N^2);–>数据逆序;
* 当一组数据,数据量不大且趋近于有序,此时用插入排序时间更快,越有序越快。
空间复杂度O(1)
稳定性:稳定的排序;
一个稳定的排序可以实现为不稳定的排序,但是一个本身就不稳定的排序不可以变成稳定的排序。

二、希尔排序

希尔排序法又称缩小增量法
希尔排序法的本质是插入排序,只不过是将待排序的序列按某种规则分成几个子序列,分别对几个子序列进行直接插入排序。这个规则就是增量,增量选取很重要,增量一般选序列长度一半,然后逐半递减,直到最后一个增量为1,为1相当于直接插入排序。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

public static void shell(int[] arr,int gap){
    for (int i = 1; i < arr.length; i++) {
        int tmp = arr[i];
        int j = i - gap;
        for (; j >= 0 ; j-=gap) {
            if(tmp < arr[j]){
                arr[j + gap] = arr[j];
            }else{
                break;
            }
        }
        arr[j+gap] = tmp;
    }
}
    public static  void shellSort(int[] arr) {
        int gap = arr.length;
        //增量在缩小,最后一组的增量为1
        while (gap > 1){
            gap /= 2;
            shell(arr,gap);

        }
        shell(arr,1);
    }
    public static void main(String[] args) {
        int[] array = {9,1,2,5,7,4,8,6,3,5};
        shellSort(array);
        System.out.println(Arrays.toString(array));

输出结果:
在这里插入图片描述

时间复杂度:O(n^1.3 - n^1.5);
空间复杂度O(1)
稳定性不稳定
看在比较的过程中,是否发生了跳跃式的交换,如果发生了跳跃式的交换,那么就是不稳定的排序。

三、选择排序

从指定元素开始,遍历其后面的元素,如遇到后面的元素比指定元素小,则进行交换,直到把整个数组遍历完,直到全部指定待排序的数据元素排完 。

public static void swap(int[] array,int i ,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
public static void selectSort(int[] array) {
        for (int i = 0; i < array.length; i++) {         
            for (int j = i + 1;; j <array.length ; j++) {
               if(array[j] < array[i]){
                   int tmp = array[i];
                   array[i] = array[j];
                   array[j] = tmp;
               }
            }
        }
    }

//优化
//遍历完指定元素后面的元素找到最小的一个数和其进行交换
 public static void selectSort1(int[] array) {
        for (int i = 0; i < array.length; i++) {
            int min = i;
            for (int j = i + 1; j <array.length ; j++) {
                if(array[j] < array[min]){
                    min = j;
                }
            }
            //两元素进行交换的函数
            swap(array,i,min);
        }
    }
 public static void main(String[] args) {
        int[] array = {9,1,2,5,7,4,8,6,3,5};
        selectSort(array);
        System.out.println(Arrays.toString(array));

稳定性:不稳定的排序
时间复杂度O(N^2),时间复杂度不等于代码的运行时间
空间复杂度O(1)

四、堆排序

基本原理也是选择排序,只是不再使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。
注意: 排升序要建大堆;排降序要建小堆
堆排序动画演示

public static void swap(int[] array,int i ,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
public static void creatHeap(int[] arr){
        for (int parenr = (arr.length -1-1)/2; parenr >= 0 ; parenr--) {
            shiftDown(arr,parenr,arr.length);
        }
    }

    public static void heapSort(int[] arr){
            //建堆O(n)
        creatHeap(arr);
        int end = arr.length - 1;
        //交换然后调整O(n*log n)
        while (end > 0){
            swap(arr,0,end);
            shiftDown(arr,0,end);
            end--;
        }
    }
    public static void shiftDown(int[] arr,int parent,int len){
        int child = 2 * parent + 1;
        while (child < len){
            if (child+1 < len && arr[child] < arr[child + 1]){
                child++;
            }
            if (arr[child] > arr[parent]){
                swap(arr,child,parent);
                parent = child;
                child = 2 * parent +1;
            }else {
                break;
            }
        }
    }
 public static void main(String[] args) {
        int[] array = {9,1,2,5,7,4,8,6,3,5};
        heapSort(array);
        System.out.println(Arrays.toString(array));

稳定性:不稳定;
空间复杂度O(1)
时间复杂度O(n*log n)

五、冒泡排序

在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序。

 public static void swap(int[] array,int i ,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
 public static void bubbleSort(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            boolean flg = false;
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if (arr[j+1] < arr[j]){
                    swap(arr,j+1,j);
                    flg = true;
                }
            }
            //如果走完一遍循环之后,flg依旧为false,则没有进行交换,此时就可以提前结束循环,提高了代码的运行时间。
            if (flg == false){
                break;
            }
        }
    }
public static void main(String[] args) {
        int[] array = {9,1,2,5,7,4,8,6,3,5};
        bubbleSort(array);
        System.out.println(Arrays.toString(array));

时间复杂度:O(N^2);有序情况下:O(N)
空间复杂度:O(1)
稳定性:稳定。

六、快速排序

原理:

  1. 从待排序区间选择一个数,作为基准值(pivot);
  2. Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
  3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度 == 0,代表没有数据。

6.1

挖坑法

主要思想为:选取数组中的第一个元素tmp作为比较的对象,然后先将整个数组从后(end位置)往前遍历:如果后面的值比tmp值大,则end往前移动,直到遇到比tmp小的元素,则将此位置的值移动到第一个元素所在的位置,然后从该位置有从前(start)往后遍历,如果start的值小于tmp,则start往后移动,直到遇到比tmp大的元素,则将此位置的值移动到end所在的位置,按照此方法,依次遍历完整个数组,即可找到基准值所在位置。

 public static void quickSort1(int[] arr) {
        quick(arr, 0, arr.length - 1);
    }

    public static void quick(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        int pivot = partition(arr, left, right);
        quick(arr, left, pivot - 1);
        quick(arr, pivot + 1, right);
    }
     public static int partition(int[] arr, int start, int end) {
        int tmp = arr[start];
        while (start < end) {
			//如果数组最后面的数比tmp大,则end往前走
            while (start < end && arr[end] >= tmp) {
                end--;
            }
            //end就遇到了小于tmp的值
            arr[start] = arr[end];
            while (start < end && arr[start] <= tmp) {
                start++;
            }
            //start就遇到了大于tmp的值
            arr[end] = arr[start];
        }
        //将tmp的值放到相遇start和end的位置
        arr[start] = tmp;
        return start;
    }

时间复杂度:最好【每次可以均匀的分割待排序序列】:O(n*log n) 最坏:【数据有序或者逆序的情况】O(n^2);
空间复杂度:最好[就是树的高度]:O(log n) 最坏:[退化成一颗单分支的树]:O(n);
稳定性:不稳定;

Hoare 法

private static int partition(int[] array, int left, int right) {
	int i = left;
	int j = right;
	int pivot = array[left];
	while (i < j) {
		while (i < j && array[j] >= pivot) {
			j--;
		}
		while (i < j && array[i] <= pivot) {
			i++;
		}
	swap(array, i, j);
	}
	swap(array, i, left);
	return i;
}

前后遍历法

private static int partition(int[] array, int left, int right) {
	int d = left + 1;
	int pivot = array[left];
	for (int i = left + 1; i <= right; i++) {
			if (array[i] < pivot) {
				swap(array, i, d);
				d++;
			}
		}
	swap(array, d, left);
	return d;
}

6.2基准值的选择

1.随机选取基准法:有可能每次随机的数据,作为基准的时候,也会出现单分支的情况;
2.选择边上(左或者右)挖坑法;
3.几数取中(例如三数取中):array[left], array[mid], array[right] 大小是中间的为基准值。
4.把基准值相同的数据,从两边移动跟前;
5.利用直接插入排序越有序越快的规则来进行优化。

6.3三数取中法示例

public static void quickSort1(int[] arr) {
        quick(arr, 0, arr.length - 1);
    }
    public static void quick(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        //找基准之前找到中间大小的数  几数取中(使用三数取中法)
        int midValue = findMidValue(arr, left, right);
        swap(arr,midValue,left);//找到中间大小的数之后和最左边的数进行交换
        int pivot = partition(arr, left, right);
        quick(arr, left, pivot - 1);
        quick(arr, pivot + 1, right);
    }
    //找基准之前找到中间大小的数  三数取中
    public static int findMidValue(int[] arr, int start, int end) {
        int mid = start + (end - start)/2;
//        int mid = start + ((end - start)>>>1);
        if (arr[start] < arr[end]){  //最前面的值小于最后面的值
            if (arr[mid] < arr[start]){ //中间的值又小于最前面的值
                return start;  //则三数中中间大的值为start位置的值
            }else if (arr[mid] > arr[end]){
                return end;
            }else {
                return mid;
            }
        }else {//最前面的值大于最后面的值
            if (arr[mid] > arr[start]){
                return start;
            }else if (arr[mid] < arr[end]){
                return end;
            }else {
                return mid;
            }
        }
    }

    public static int partition(int[] arr, int start, int end) {
        int tmp = arr[start];
        while (start < end) {

            while (start < end && arr[end] >= tmp) {
                end--;
            }
            //end就遇到了小于tmp的值
            arr[start] = arr[end];
            while (start < end && arr[start] <= tmp) {
                start++;
            }
            //start就遇到了大于tmp的值
            arr[end] = arr[start];
        }
        //将tmp的值放到相遇start和end的位置
        arr[start] = tmp;
        return start;
    }

6.4直接插入法排序优化示例

 public static void insertSort2(int[] arr,int start,int end ) {
        for (int i = 1; i < end; i++) {
            int tmp = arr[i];
            int j = i - 1;
            for (; j >= start; j--) {
                if (arr[j] > tmp) {
                    arr[j + 1] = arr[j];
                } else {
                    //arr[j + 1] = tmp;//只要j回退的时候,遇到了比tmp小的元素,就结束了这次的比较
                    break;
                }
            }
            //j回退到了小于0的地方
            arr[j + 1] = tmp;
        }
    }

    public static void quickSort1(int[] arr) {
        quick(arr, 0, arr.length - 1);
    }

    public static void quick(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        //如果区间内的数字,在排序的过程当中,小于某个范围,可以使用直接插入排序
        if (left - right + 1 <= 40) {  //40这个数可以自己规定
            //使用直接插入排序
            insertSort2(arr,left,right);
            return;
        }
        //找基准之前找到中间大小的数  几数取中(使用三数取中法)
        int midValue = findMidValue(arr, left, right);
        swap(arr,midValue,left);//找到中间大小的数之后和最左边的数进行交换
        int pivot = partition(arr, left, right);
        quick(arr, left, pivot - 1);
        quick(arr, pivot + 1, right);
    }
    //找基准之前找到中间大小的数  三数取中
    public static int findMidValue(int[] arr, int start, int end) {
        int mid = start + (end - start)/2;
//        int mid = start + ((end - start)>>>1);
        if (arr[start] < arr[end]){  //最前面的值小于最后面的值
            if (arr[mid] < arr[start]){ //中间的值又小于最前面的值
                return start;  //则三数中中间大的值为start位置的值
            }else if (arr[mid] > arr[end]){
                return end;
            }else {
                return mid;
            }
        }else {//最前面的值大于最后面的值
            if (arr[mid] > arr[start]){
                return start;
            }else if (arr[mid] < arr[end]){
                return end;
            }else {
                return mid;
            }
        }
    }

    public static int partition(int[] arr, int start, int end) {
        int tmp = arr[start];
        while (start < end) {

            while (start < end && arr[end] >= tmp) {
                end--;
            }
            //end就遇到了小于tmp的值
            arr[start] = arr[end];
            while (start < end && arr[start] <= tmp) {
                start++;
            }
            //start就遇到了大于tmp的值
            arr[end] = arr[start];
        }
        //将tmp的值放到相遇start和end的位置
        arr[start] = tmp;
        return start;
    }

快速排序优化方法:

  1. 选择基准值很重要,通常使用几数取中法;
  2. partition 过程中把和基准值相等的数也选择出来;
    就是把数组中和基准值相等的数都移动到基准值的周围,这样与基准值相等的值都汇集到了一起,然后只需遍历与基准值不相等的前面和后面即可。
  3. 待排序区间小于一个阈值时(例如 40),使用直接插入排序;

6.5非递归实现快速排序

1.找基准
2.栈 划分之后把左右的数都放到队列中,划分前提:pivot左边有两个元素 pivot > left +1,右边有两个元素
3.判断栈是否为空,如果栈不为空,则依次弹出栈中的两个元素,分别作为数组的右边元素和左边元素,然后再找基准,直到栈为空为止。

public static void quickSort(int[] arr) {
        Stack<Integer> stack = new Stack<>();
        int left = 0;
        int right = arr.length - 1;
        int pivot = partition(arr, left, right);
        //划分之后把左右的数对都放到栈当中
        //前提:pivot左边有两个元素  右边有两个元素
        if (pivot > left + 1) {
            stack.push(left);
            stack.push(pivot - 1);
        }
        if (pivot < right - 1) {
            stack.push(pivot + 1);
            stack.push(right);
        }
        while (!stack.isEmpty()) {
            right = stack.pop();
            left = stack.pop();
            pivot = partition(arr, left, right);
            if (pivot > left + 1) {
                stack.push(left);
                stack.push(pivot - 1);
            }
            if (pivot < right - 1) {
                stack.push(pivot + 1);
                stack.push(right);
            }
        }
    }

七、归并排序

归并排序MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

首先,先写一下两个有序数组合并为一个有序数组的代码:

 public static int[] mergeArray(int[] arr1, int[] arr2) {
        //判断数组是否为空
        int[] tmp = new int[arr1.length + arr2.length];
        int k = 0;//代表tmp数组的下标
        int s1 = 0;
        int e1 = arr1.length - 1;
        int s2 = 0;
        int e2 = arr2.length - 1;
        while (s1 <= e1 && s2 <= e2) {
            if (arr1[s1] <= arr2[s2]) {
                tmp[k] = arr1[s1];
                k++;
                s1++;
            } else {
                tmp[k++] = arr2[s2++];
            }
        }
        while (s1 <= e1) {
            tmp[k++] = arr1[s1++];
        }
        while (s2 <= e2) {
            tmp[k++] = arr2[s2++];
        }
        return tmp;
    }

    public static void main(String[] args) {
        int[] array1 = {1,3,5,7,9};
        int[] array2 = {2,4,6,8,10};
       int[] ret = mergeArray(array1,array2);
        System.out.println(Arrays.toString(ret));
    }

输出结果:
在这里插入图片描述

7.1归并排序代码示例

 public static void mergeSort(int[] arr) {
        mergeSortInternal(arr, 0, arr.length - 1);
    }

    private static void mergeSortInternal(int[] arr, int low, int high) {
        if (low >= high) {
            return;
        }
        int mid = low + ((high - low) >>> 1);//无符号右移一位
        //左边
        mergeSortInternal(arr, low, mid);
        //右边
        mergeSortInternal(arr, mid + 1, high);
        //合并
        merge(arr, low, mid, high);
    }

    private static void merge(int[] arr, int low, int mid, int high) {
        int s1 = low;
        int e1 = mid;
        int s2 = mid + 1;
        int e2 = high;
        int[] tmp = new int[high - low + 1];
        int k = 0;

        while (s1 <= e1 && s2 <= e2) {
            if (arr[s1] <= arr[s2]) {
                tmp[k++] = arr[s1++];
//                k++;
//                s1++;
            } else {
                tmp[k++] = arr[s2++];
            }
        }
        while (s1 <= e1) {
            tmp[k++] = arr[s1++];
        }
        while (s2 <= e2) {
            tmp[k++] = arr[s2++];
        }
        //拷贝tmp数组的元素放到arr里面
        for (int i = 0; i < k; i++) {
            arr[i + low] = tmp[i];
        }
    }

    public static void main(String[] args) {
        int[] arr = {3,5,1,2,34,0,98,-1,56};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }

输出结果:
在这里插入图片描述
时间复杂度:O(n*logn)
空间复杂度:O(n)
稳定性:稳定的排序

7.2非递归方法实现归并排序

 public static void mergeSort1(int[] arr) {
        int nums = 1;//每组数据的个数
        while (nums < arr.length){
            //数组每次都要进行遍历,确定要归并的区间
            for (int i = 0; i < arr.length; i+=nums * 2) {
                int left = i;
                int mid = left + nums - 1;
                if (mid >= arr.length){//防止越界
                    mid = arr.length - 1;
                }
                int right = mid + nums;
                if (right >= arr.length){//防止越界
                    right  = arr.length - 1;
                }
                //下标确定之后,合并
                merge(arr,left,mid,right);
            }
            nums *= 2;
        }
    }
public static void main(String[] args) {
        int[] arr = {3,5,1,2,34,0,98,-1,56};
        mergeSort1(arr);
        System.out.println(Arrays.toString(arr));
    }

输出结果:
在这里插入图片描述

八、海量数据的排序问题

外部排序:排序过程需要在磁盘等外部存储进行的排序。
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序

  1. 先把文件切分成 200 份,每个 512 M;
  2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以;
  3. 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了。

关于各种排序算法的时间复杂度、空间复杂度及稳定性列表

排序算法最好时间复杂度最坏时间复杂度最好空间复杂度最坏空间复杂度稳定性
直接插入排序O(N)O(N^2)O(1)O(1)稳定
希尔排序O(N^1.3)O(N^1.5)O(1)O(1)不稳定
选择排序O(N^2)O(N^2)O(1)O(1)不稳定
堆排序O(NlogN)O(NlogN)O(1)O(1)不稳定
冒泡排序O(N^2)O(N^2)O(1)O(1)稳定
快速排序O(NlogN)O(N^2)O(logN)O(N)不稳定
归并排序O(NlogN)O(NlogN)O(N)O(N)稳定

其他非基于比较的排序

基数排序

桶排序
在这里插入图片描述
计数排序

 public static void countSort(int[] arr){
        int maxValue = arr[0];
        int minValue = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] < minValue){
                minValue = arr[i];
            }
            if (arr[i] > maxValue){
               maxValue = arr[i];
            }
        }
        int[] count = new int[maxValue-minValue + 1];
        for (int i = 0; i < arr.length ; i++) {
            int index = arr[i];
            //为了空间的合理使用,需要减去minValue
            count[index - minValue]++;
        }
        //说明在计数数组当中已经把arr数组中每个数据出现的次数已经统计好了
        //接下来遍历计数数组把数据写回arr即可
        int indexArray = 0;
        for (int i = 0; i < count.length; i++) {
            while (count[i] > 0){
                arr[indexArray] = i + minValue;
                count[i]--;//拷贝一个之后,此时也就是少一个
                indexArray++;//
            }
        }
    }

计数排序 一般适用于有n个数;
时间复杂度:O(N)
空间复杂度:O(M) M代表当前数据的范围;
稳定性:不稳定, 当前代码是不稳定的,但是本质是稳定的;


以上。
  • 16
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值