Java排序(七大排序合集)

排序是算法中有着很重要的地位。它会涉及到很多方面的知识,不仅仅是算法相关的知识,还有很多很数据结构相关的知识,所以了解排序,对于我们学习算法和数据结构都是很有帮助的。
其中常见的排序有其中排序方法,分别为冒泡排序、选择排序、插入排序、希尔排序、堆排序、归并排序、快速排序,本文则是围绕着着其中排序方法进行讲解。

1、冒泡排序

冒泡排序(Bubble Sort)因其易于理解,作为不少初学者学习到的第一个的排序算法。
冒泡排序之所以叫为冒泡,是因为其想泡泡一样,一步步地将大的泡泡向上冒出,在数据中表现则为将较大的数字一步步交换到无序空间的后面。

1.1、排序过程图

冒泡排序过程图

1.2、排序思想

1.依次比较每两个相邻的数的大小,将较大的数交换到后面。
2.当第一次整个数据比较和交换完成后,整个数组中最大的数字则被排在数组的最后。
3.此时,已经将一个数排到了其相应的位置,不需要再移动了。
4.再次遍历整个数组,将次大的数通过比较和交换,排在其相应的位置。
5.重复上述流程,知道整个数组都被排序。

1.3、排序代码

public static void bubbleSort(int[] arr){
	//因为排好数组中第二个数时,第一个数也自然排好了,所以遍历的此数为length - 1
    for (int i = 0; i < arr.length - 1; i++) {
    	//第i + 1次遍历整个数组
        for (int j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]){
                swap(arr, j, j + 1);
            }
        }
        //此时倒数第i + 1个数则排序好了
    }
}
private static void swap(int[] arr, int i, int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

1.4、代码改进

上述的代码任然有一定改进的空间:若输入的数组已经完整排序,此时则不需要再排序,但上述的代码并不能实现这一点,于是可以加上一个对数组排序的判断。

public static void bubbleSort(int[] arr){
	//isSorted 用于判断数组是否已经排序完成
    boolean isSorted = true;
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]){
                swap(arr, j, j + 1);
                //此时数组仍未排序完成
                isSorted = false;
            }
        }
        //若排序完成,则 isSorted 应该为true,则提前跳出循环
        if (isSorted){
            break;
        }
    }
}
private static void swap(int[] arr, int i, int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

2、选择排序

选择排序(Selection Sort),顾名思义,就是每次遍历的时候,都选择一个最小(大)的数进行排序,直到此数排到相应的位置,结束此数的排序,再选除此数外最小(大)的数进行排序。

2.1、排序过程图

选择排序过程图

2.2、排序思想

1.初次遍历数组时,根据依次比较,筛选出当前数组中最小的数。
2.将此数通过交换到数组最前的位置,此数则排序完成。
3.再将除排序好的数以外的数组,再次筛选出最小值。
4.再次将当前最小值通过交换,排到数组的最前。
5.重复上述流程,直到整个数组排序完成。

2.3、排序代码

public static void selectSort(int[] arr){
	//依次筛选并排序最小值
    for (int i = 0; i < arr.length - 1; i++) {
    	//用min记录当前数组最小值的索引
        int min = i;
        //遍历数组,筛选出最小值得索引
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[min]){
                min = j;
            }
        }
        //交换最小值和当前数组最前的位置的值
        swap(arr, min, i);
        //此时当前数组最小值得排序完成
    }
}
private static void swap(int[] arr, int i, int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

2.4、代码改进——双向选择排序

在上述的代码,即单向选择排序中,一次遍历数组,只能筛选出一个数进行排序,效率略低,则可以考虑一次遍历筛选出两个数,即可以筛选出最小值和最大值。

2.4.1、改进排序思想

1.在每次遍历数组时,筛选出当前数组中的最小值和最大值。
2.将最小值和最大值分别交换到当前数组的最前和最后。
3.再将除已排序完的数的数组,再次按照上述流程进行排序。
注:
可能会出现max和low重合的情况,即max的值并未变化的情况,则此时min才是max应该指向的位置。

2.4.2、改进排序代码

public static void selectSortOP(int[] arr){
    //记录最小值得索引
    int low = 0;
    //记录最大值的索引
    int high = arr.length - 1;
    while (low <= high){
    	//从左边开始遍历
        int min = low;
        int max= low;
        for (int i = low + 1; i <= high; i++) {
        	//筛选出更小的数
            if (arr[i] < arr[min]){
                min = i;
            }
            //筛选出更大的值
            if (arr[i] > arr[max]){
                max = i;
            }
        }
        //交换数组最左边的值和最小值
        swap(arr, min, low);
        //max和low重合的情况
        if (max == low){
            max = min;
        }
        //交换数组最右边的值和最大值
        swap(arr, max, high);
        //移动左右指针
        ++low;
        --high;
    }
}
private static void swap(int[] arr, int i, int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

3、插入排序

插入排序(Insertion Sort)是排序算法中稳定的排序算法,它不会改变相同数字之间的相对位置,而且这种排序算法, 和选择排序有点类似,若已理解了选择排序,那插入排序的理解会变得相对简单。插入排序的主要思想,是将无序区间里的数依次插入到有序区间里的对应位置,并不能改变有序区间的有序性。

3.1、排序过程图

插入排序过程图

3.2、排序思想

1.在排序时,有序区间为[ 0 , i ),无序区间为[ i , arr.length ),注意两区间都是左闭右开的区间。
2.选择当前无序区间的第一个数,和前面的数依次比较,若前面的数比当前的数大,则交换,直到遇到遇到一个小于它的数,或已经到达数组的最前位置。
3.此时,排好了一个数,当前的有序区间为[ 0 , i + 1),[ i - 1, arr.length )。
4.并继续按照上述流程比较和交换,直到整个数据排序完成。

3.3、排序代码

public static void insertSort(int[] arr){
    for (int i = 1; i < arr.length; i++) {
        for (int j = i; j >= 1 && arr[j] < arr[j - 1]; j--) {
            swap(arr, j, j - 1);
        }
    }
}
private static void swap(int[] arr, int i, int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

3.4、代码改进——折半插入排序

上述的代码在查找其相应的位置时,效率略微低下,于是可选择常用的二分查找,来帮助我们找到其应该移动到的位置,提高效率。

3.4.1、改进代码思想

1.首先仍然是要找到当前无序区间中的第一个数。
2.通过二分查找,找到其应该移动到的位置。
3.将有序数组从其应该移动到的位置开始,到数组末尾的数,全部向后搬移一个位置。
4.将数字直接插入到相应位置。

3.4.2、改进代码

public static void insertSortBS(int[] arr){
	// 有序区间[0..i)
    // 无序区间[i...n]
    for (int i = 1; i < arr.length; i++) {
        int left = 0;
        int right = i;
        int val = arr[i];
        //使用二叉查找,找到应插入到的位置。
        while (left < right){
            int mid = (left + right) >> 1;
            if (val < arr[mid]){
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        // 搬移left..i的元素
        for (int j = i; i > left; i--){
            arr[j] = arr[j - 1];
        }
        // left就是val插入的位置
        arr[left] = val;
    }
}

4、希尔排序

希尔排序(Shell Sort)又称缩小增量法。它的思想和插入排序的思想有一定的关联。插入排序是将整个数组看成一个整体,而希尔排序是将整个数组划分为几大块,先每块分别进行插入排序,再将几个大块拆分为更多的小块,并继续上述的操作,直到拆分到每块都只有一个元素,此时整个数组则排序完成。

4.1、排序流程图

希尔排序流程图

4.2、排序思想

1.先将整个数组对半拆分,得到两部分数组。
2.对那两部分数组进行插入排序。
3.再将每块进行对半拆分,得到几部分的数组。
4.再次对那几部分的数组进行插入排序。
5.直到每部分都只有一个元素,此时已无法再拆分,则整个数组都排序完成。

4.3、排序代码

public static void shellSort(int[] arr){
	//首次拆分
    int gap = arr.length >> 1;
    while (gap > 1){
    	//进行插入排序
        insertSortByGap(arr, gap);
        //对每部分进行拆分
        gap >>= 1;
    }
    //此时整个数组已经近乎有序,再次使用插入排序即可有序
    insertSort(arr);
}
//此函数为将插入排序中的起始索引1,改为gap
private static void insertSortByGap(int[] arr, int gap) {
    for (int i = gap; i < arr.length; i++) {
    	//对每部分的数组进行插入排序
        for (int j = i; j >= gap && arr[j] > arr[j - gap]; j -= gap) {
            swap(arr, j, j - gap);
        }
    }
}

5、堆排序

堆排序(Heap Sort),顾名思义,就是要用到堆的知识。其基本的原理也就是选择排序,知识不再使用遍历的方式查找无序区间的最大数,而是用堆来来选择无序区间中的最大值。但是需要注意的是,排升序需要用到最大堆,排降序需要用到最小堆。

5.1、排序流程图

堆排序的排序流程图

5.2、排序思想

在讲解堆排序思想前,需要先知道一些关于堆排序的知识:
1.堆在逻辑上事一颗完全二叉树。
2.堆在物理上是保存在数组中(按照层序遍历顺序保存)。
3.最大堆:每一个节点的值都大于或等于其两个子节点的值。
4.最大堆的下沉操作:为满足最大堆的性质,需要将指定位置的数在二叉树的逻辑上,向下交换到合适的位置。
5.最大堆的构建:从层序遍历中最后一个非叶子节点开始,每个节点执行下沉操作,则可完成二叉树的构建。
在堆排序中,需要用到以上的有关于堆的知识,此时来讲解堆排序:
1.将无序数组构建为最大堆的形式,此时堆顶的数就是当前无序数组中最大的数。
2.将无序数组的最后一个数和第一个数交换,则最大的数被交换到了无序数组的最后,成为了有序数组的一部分。
3.将交换后的第一个数(也就是刚刚无序数组的最后一个数)进行下沉操作,以继续满足最大堆的性质。
4.重复上述的操作,直到整个数组排序完成。

5.3、排序代码

public static void heapSort(int[] arr){
	//从最后一个非叶子节点开始下沉操作,构建最大堆
    for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--){
        siftDown(arr, i, arr.length);
    }
    //对数组进行交换和下沉操作
    for (int i = arr.length - 1; i > 0; i--){
        swap(arr, 0, i);
        siftDown(arr, 0, i);
    }
}
//下沉操作
private static void siftDown(int[] arr, int i, int length) {
	//保证此节点有左节点
    while (2 * i + 1 < length){
    	//左节点的索引
        int j = (i << 1) + 1;
        //判断是否有右节点,并筛选出两节点的最大值所在的索引
        if (j + 1 < length && arr[j + 1] > arr[j]){
            ++j;
        }
        //若节点的值小于节点的最大值,则结束循环
        if (arr[i] > arr[j]){
            break;
        }
        //交换两节点的值,并更改当前节点的索引
        swap(arr, i, j);
        i = j;
    }
}

6、归并排序

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

6.1、排序过程图

归并排序过程图

6.2、排序思想

归并排序的主要思想就是先分割,再合并。
1.将整个数组折半分割,直到每个子数组都只有一个数。
2.再将每两个相邻的子数组结合。
3.创建一个新数组,来暂时保存数组。
4.一次比较两子数组中的值,将较小的值依次放入新数组中。
5.再将剩下未放入到新数组中的数,依次放入到新数组中。
6.当整个新数组都放满时,再用新数组的值覆盖掉之前数组的值。
3.不停地结合并排序,直到整个数组合并完成,则排序完成。

6.3、排序代码

public static void mergeSort(int[] arr){
	//数组的区间为闭区间
    mergeSortInternal(arr, 0, arr.length - 1);
}
private static void mergeSortInternal(int[] arr, int l, int r) {
    //求中间索引
    int mid = r + ((l - r) >> 1);
    //将左数组排序
    mergeSortInternal(arr, l, mid);
    //将右数组排序
    mergeSortInternal(arr, mid + 1, r);
    //若数组已排序成功,则返回
    //若数组还为排序成功,则将左右数组合在一起归并排序
    if (arr[mid] > arr[mid + 1]){
        merge(arr, l, mid, r);
    }
}
private static void merge(int[] arr, int l, int mid, int r) {
	//创建新数组,来暂时保存数组
    int[] newArr = new int[r - l + 1];
    //左数组的第一个索引
    int i = l;
    //右数组的第一个索引
    int j = mid + 1;
    //辅助数组的第一个索引
    int k = 0;
    //依次比较出较小的数,并放入到新数组中
    while (i <= mid && j <= r){
        if (arr[i] <= arr[j]){
            newArr[k++] = arr[i++];
        } else {
            newArr[k++] = arr[j++];
        }
    }
    //前数组有剩余的数
    while (i <= mid){
        newArr[k++] = arr[i++];
    }
    //后数组有剩余的数
    while (j <= r){
        newArr[k++] = arr[j++];
    }
    //将辅助数组的值赋给原数组
    for (int m = 0; m < newArr.length; m++) {
        arr[l + m] = newArr[m];
    }
}

6.4、代码改进——解决可能的栈溢出问题

6.4.1、代码改进思想

归并排序在执行时,需要多次调用函数,而当数据过多时,可能会出现栈溢出的问题,于是,可以在数组长度较短时,使用插入排序(在近乎有序的情况下,插入排序的效率高),来解决栈溢出的问题。

6.4.2、改进代码

public static void mergeSort(int[] arr){
	//数组的区间为闭区间
    mergeSortInternal(arr, 0, arr.length - 1);
}
private static void mergeSortInternal(int[] arr, int l, int r) {
    //若当前数组偿付较短时,使用插入排序,否则容易栈溢出
    if (r - l <= 15){
        insertSort(arr, l, r);
        return;
     }
    //求中间索引
    int mid = r + ((l - r) >> 1);
    //将左数组排序
    mergeSortInternal(arr, l, mid);
    //将右数组排序
    mergeSortInternal(arr, mid + 1, r);
    //若数组已排序成功,则返回
    //若数组还为排序成功,则将左右数组合在一起归并排序
    if (arr[mid] > arr[mid + 1]){
        merge(arr, l, mid, r);
    }
}
private static void merge(int[] arr, int l, int mid, int r) {
	//创建新数组,来暂时保存数组
    int[] newArr = new int[r - l + 1];
    //左数组的第一个索引
    int i = l;
    //右数组的第一个索引
    int j = mid + 1;
    //辅助数组的第一个索引
    int k = 0;
    //依次比较出较小的数,并放入到新数组中
    while (i <= mid && j <= r){
        if (arr[i] <= arr[j]){
            newArr[k++] = arr[i++];
        } else {
            newArr[k++] = arr[j++];
        }
    }
    //前数组有剩余的数
    while (i <= mid){
        newArr[k++] = arr[i++];
    }
    //后数组有剩余的数
    while (j <= r){
        newArr[k++] = arr[j++];
    }
    //将辅助数组的值赋给原数组
    for (int m = 0; m < newArr.length; m++) {
        arr[l + m] = newArr[m];
    }
}
//插入排序
public static void insertSort(int[] arr,int l,int r){
    for (int i = l + 1; i <= r; i++) {
        for (int j = i; j > l && arr[j] < arr[j - 1]; j--){
            swap(arr, j - 1, j);
        }
    }
}
private static void swap(int[] arr, int i, int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

7、快速排序

快速排序(Quick Sort)是排序算法中很重要,而且也是使用很广泛的一种排序算法。它的思想也归并排序有点类似,都是需要分区,并对每个分区进行排序,然后对整个分区进行排序。

7.1、排序流程图

java快速排序流程图

7.2、排序思想

1.先对整个数组进行分区。
2.在分区时便可以对数组进行一定程度的排序。
3.从待排序的区间中选择一个数,作为目前整个区间的基准值。
4.遍历整个待排序区间,将比基准值小的(可以包括相等的)放在基准值的左边,将比基准值打的(也可以包括相等的)放到基准值的右边。
5.采用分治的思想,对左右的两个小区间也使用同样的方式处理,直到小区间的长度为1,代表已经有序,或者小区间的长度为0,代表没有数据。
6.重复上述的流程,直到整个数组排序完成。

7.3、排序代码

public static void quickSort(int[] arr){
	//对数组进行快速排序,区间为闭区间
    quickSortInternal(arr, 0, arr.length - 1);
}

//对数组分区
private static void quickSortInternal(int[] arr, int l, int r) {
	//将当前的数组进行快速排序
    int p = partition(arr, l, r);
    //对左区间进行快速排序
    quickSortInternal(arr, l, p);
    //对右区间进行快速排序
    quickSortInternal(arr, p + 1, r);
}

//对分好区的子数组进行快速排序
private static int partition(int[] arr, int l, int r) {
	//用v来记录基准值
    int v = arr[l];
    //j为小于基准值的区间
    int j = l;
    for (int i = l + 1; i <= r; i++) {
    	//若小于基准值,则将数字移动到小于的区间中
        if (arr[i] < v){
            swap(arr, i, ++j);
        }
    }
    //将基准值交换到相应的位置
    swap(arr, l, j);
    return j;
}

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

7.4、代码改进1——防止栈溢出和加入随机数

7.4.1、代码改进思想

1.上述的代码在较少的数据处理时,一般不会有什么问题,但当遇到很多的数据时,就可能会出现栈溢出的问题。于是可以在处理较少的数据时,直接使用插入排序(插入排序在近乎有序时,效率很高),减少对函数的调用。
2.在进行快速排序选择基准值时,可以使用随机数,使得排序会更加有效。

7.4.2、改进代码

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

//对数组分区
private static void quickSortInternal(int[] arr, int l, int r) {
	//在较少的数据时,使用插入排序,
    if (r - l <= 15){
        insertSort(arr, l, r);
        return;
    }
    int p = partition(arr, l, r);
    quickSortInternal(arr, l, p);
    quickSortInternal(arr, p + 1, r);
}

//对分好区的子数组进行快速排序
private static int partition(int[] arr, int l, int r) {
	//加入随机数,使得此次的排序会更加有效
    int randomIndex = random.nextInt(l, r);
    swap(arr, l, randomIndex);
    int v = arr[l];
    int j = l;
    for (int i = l + 1; i <= r; i++) {
        if (arr[i] < v){
            swap(arr, i, ++j);
        }
    }
    swap(arr, l, j);
    return j;
}

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

public static void insertSort(int[] arr,int l,int r){
    for (int i = l + 1; i <= r; i++) {
        for (int j = i; j > l && arr[j] < arr[j - 1]; j--){
            swap(arr, j - 1, j);
        }
    }
}

7.5、代码改进2——二路快排

7.5.1、代码改进流程图

二路快排流程图

7.5.2、代码改进思想

之前的代码都是一次遍历只能选择一个数放入到对应的区间,于是我们便可以想到是否可以一次遍历,选出两个数来放入到对应的区间,以提高遍历的效率
1.使用左右两个指针,分别指向待排序数据的起始和结尾的位置,左指针向右遍历,右指针向左遍历。
2.当左指针找到一个大于基准值的数,且右指针找到第一个小于基准值的数时,两数据进行交换。
3.重复上述的流程,直到两指针相遇,则遍历结束。
4.将基准值交换到相应的位置。

7.5.3、改进代码

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

private static void quickSortInternal2(int[] arr, int l, int r) {
    if (l - r <= 15){
        insertSort(arr, l, r);
        return;
    }
    int p = partition2(arr, l, r);
    quickSortInternal2(arr, l, p);
    quickSortInternal2(arr, p + 1, r);
}

private static int partition2(int[] arr, int l, int r) {
    int randomIndex = random.nextInt(l, r);
    swap(arr, l, randomIndex);
    int v = arr[l];
    //左指针
    int m = l;
    //右指针
    int n = r;
    while (m < n){
    	//左指针一直遍历,直到找到一个大于基准值的数
        while (m < n && arr[m + 1] < v){
            ++m;
        }
        //右指针一直遍历,直到找到一个小于基准值的数
        while (m < n && arr[n - 1] > v){
            --n;
        }
        //交换左右指针所指向的数
        swap(arr, m, n);
        ++m;
        --n;
    }
    swap(arr, l, m);
    return m;
}

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

public static void insertSort(int[] arr,int l,int r){
    for (int i = l + 1; i <= r; i++) {
        for (int j = i; j > l && arr[j] < arr[j - 1]; j--){
            swap(arr, j - 1, j);
        }
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值