数据结构——排序

引言

排序的稳定性

对于一组待排序的数据,数据中有两个或两个以上的相同的元素,在经过排序后,这两个或两个以上的相同元素与之前未排序的顺序相同,则说明此排序操作是稳定的。

举例说明:

在下图中,待排序数据 ① 经过排序成为 ② ,我们发现绿色数字5和红色数字5与排序前的顺序并未发生改变,那么排序 ② 就是稳定的。
反之,排序 ③ 就是不稳定的。
分析

三个稳定的排序

本篇博客所提到的稳定的排序:直接插入、冒泡、归并。

一、直接插入排序

思想:

从数组的第二个元素开始,将当前元素存为临时变量,然后与当前下标之前的位置进行比较元素,大的放后面,小的放前面。

/**
 * 直接插入排序
 * 时间复杂度(最坏情况下):O(n^2)
 * 时间复杂度最好情况下:O(n),即一组数据已经有序了
 * 所以,对于插入排序来说,待排序的一组数据趋于有序,那么排序的速度越快
 * 空间复杂度:O(1)
 * 是否稳定:稳定
 */
public class Test {
    public static void main(String[] args) {
        int[] arr = {8,5,9,4,7};
        insertSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void insertSort(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            int temp = arr[i];
            int j = i-1;
            for (; j >= 0 ; j--) {
                if(arr[j] > temp){ //前面的元素比后面的元素大,就交换
                    arr[j+1] = arr[j];
                }else {
                    break;
                }
            }
            arr[j+1] = temp;
        }
    }
	
	//写成这样更简洁
	public static void insertSort2(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            int temp = arr[i];
            int j = i-1;
            for (; j >= 0 && arr[j] > temp; j--) {
                arr[j+1] = arr[j];//前面的元素比后面的元素大,就交换
            }
            arr[j+1] = temp;
        }
    }

输出结果:

out

分析

二 、希尔排序

希尔排序本质上就是直接插入排序,只不过对其进行了分组。当我们给定了一组数据,直接插入排序直接对这组数据进行了排序,而希尔可以将其先分成某个组别,之后对这些组别分别进行排序操作。

让我们来看下面的一个例子,可以发现每当我们进行一次排序后,小的数字都比较靠前,而大的数字都比较靠后,这就会致使下一次的排序速度的更快。
关于时间复杂度这里我还没弄懂,不过大话数据结构这本书上说明:不同的增量序列所对应的时间复杂度不同,增量序列实际上对应的就是我们进行排序时分的组别元素的个数,这和算法很有关系,我暂且不作考虑了。。。

但是有一点很重要,就是:不管我们分了多少组,每一组有多少元素,我们都必须在最后的时候,分为一组。也就是说:我们最后要对整组数据进行排序。这很好理解,因为不论我们怎么分组排序,最后很大可能都不会把一组数据排序好我们想要的结果,但是我们最后一次排序的时候一定会快很多,因为最后一组数据非常趋近于我们想要的排序序列!
分析

/**
 * 希尔排序
 * 时间复杂度[ 和增量是有关系的 ]:O(n^1.3) - O(n^1.5)
 * 空间复杂度:O(1)
 * 稳定性:不稳定
 * 希尔排序在排序的过程中,发生了跳跃性的交换元素,
 * 在最坏的情况下,就很有可能发生了排序后的相同元素与排序前的顺序不匹配
 */
public class Test {
    public static void main(String[] args) {
        int[] arr = {4,12,5,8,9,14,11,6,15,2,13,1,3,10,7};
        shellSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    //希尔排序,传入 gap 进行分组
    public static void shellSort(int[] arr){
        int gap = arr.length;
        while(gap > 1){
            shell(arr, gap);
            gap = gap / 2; //[7组,3组,1组]
        }
        shell(arr, 1);
    }
    //开始排序
    public static void shell(int[] arr, int gap){
        for (int i = gap; i < arr.length; i++) {
            int temp = arr[i];
            int j = i - gap;
            for (; j >= 0; j = j-gap) {
                if(arr[j] > temp){
                    arr[j+gap] = arr[j];
                }else {
                    break;
                }
            }
            arr[j+gap] = temp;
        }
    }
}

输出结果:

out

三、选择排序

思想:

每一趟排序后,第一个元素一定是最小的。

/**
 * 时间复杂度:O(n^2)
 * 时间复杂度在最坏情况下或者是最好情况下都是 O(n^2)
 * 空间复杂度:O(1)
 * 稳定性:不稳定
 */
public class Test {
    public static void main(String[] args) {
        int[] arr = {8,5,9,4,7};
        selectSort2(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void selectSort(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            for(int j = i+1; j < arr.length; j++){
                if(arr[i] > arr[j]){
                    int temp = arr[j];
                    arr[j] = arr[i];
                    arr[i] = temp;
                }
            }
        }
    }

	//选择排序优化
    public static void selectSort2(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            int minIndex = i;
            for(int j = i+1; j < arr.length; j++){
                if(arr[j] < arr[minIndex]){
                    minIndex = j;
                }
            }
            if(minIndex != i){ //如果两个数相同,就不进行交换
                int temp = arr[minIndex];
                arr[minIndex] = arr[i];
                arr[i] = temp;
            }
        }
    }
}

输出结果:

out

四、堆排序

堆排序在我之前的博客上有介绍,这里就不再赘述,主要阐明一下思想。

思想:

① 如果我们将待排序的一组数据进行升序,那么我们就先创建一个大顶堆,目的是始终将堆顶的元素置为最大值。
② 堆排序的实际操作就是将堆顶元素与堆尾元素进行交换。
③ 每次堆排序过后,就立即调整一次,调整为大顶堆,以便下一次排序。

/**
 * 堆排序
 * createMaxHeap 时间复杂度:O(n)
 * shiftDown 时间复杂度:O(log n)
 * heapSort 时间复杂度:O(n * log n) [ heapSort 函数里面嵌套了 shiftDown 函数 ]
 * =>总时间复杂度:O(n * log n) [ O(n) + O(n * log n) ]
 *
 * 空间复杂度:O(1)
 * 稳定性:跳跃式排序,不稳定(试想极端条件下,待排序的每个数据都为1)
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {8,5,9,4,7};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        createMaxHeap(arr);
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    //堆排序
    public static void heapSort(int[] arr){
        int end = arr.length - 1;
        while (end > 0){
            //1. 先交换
            int temp = arr[0];
            arr[0] = arr[end];
            arr[end] = temp;

            //2. 后调整
            //每次调整的其实都是树的根节点及根节点向下
            shiftDown(arr,0, end);
            end--;
        }
    }

    //创建一个大顶堆,目的是始终将堆顶的元素置为最大值
    public static void createMaxHeap(int[] arr){
        int child = arr.length - 1;
        //处理每棵树的顺序
        for (int parent = (child-1)/2; parent >= 0 ; parent--) {
            shiftDown(arr,parent,arr.length);
        }
    }

	//向下调整
    public static void shiftDown(int[] arr, int parent, int len){
        int child = parent*2 + 1;
        while(child < len){
            if(child+1 < len && arr[child] < arr[child+1]){
                child++;
            }
            if(arr[child] > arr[parent]){
                int temp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = temp;
            }
            parent = child;
            child = parent*2 + 1;
        }
    }
}

输出结果:

out

五、冒泡排序

思想:

将紧挨着的数据进行比较,一趟代表遍历完数组一次,而每一趟冒泡排序之后,最大的元素一定放在最后一位。

分析
分析2
优化分析:

在内层 for 循环中的 if 语句中,判断布尔类型 isSorted ,如果进入了 if 语句,说明进行了交换,isSorted 置为 false;反之,如果某一趟的 if 语句一次也没进去,isSorted 就为初始的 true,说明从这一趟往后,数据已经有序。
分析

/**
 * 冒泡排序
 * 时间复杂度:O(n^2)
 * 空间复杂度:O(1)
 * 稳定性:稳定
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {8,5,9,4,7};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        bubbleSort2(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void bubbleSort(int[] arr){
        for (int i = 0; i < arr.length-1; i++) { //假设待排序有5个数据,只比较了4趟
            for (int j = 0; j < arr.length-1-i; j++) { //每一趟比较了多少次
                if(arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }

	//冒泡排序优化
    public static void bubbleSort2(int[] arr){
        //举个例子 arr = {4,3,5,6,7}, 第一趟冒泡排序结束以后,实际上已经有序
        for (int i = 0; i < arr.length-1; i++) { 
            boolean isSorted = true;
            for (int j = 0; j < arr.length-1-i; j++) {
                if(arr[j] > arr[j+1]){
                //若某一趟一次也没有交换,那么isSorted 还是为 true,即数组已经有序
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                    isSorted = false;
                }
            }
            if(isSorted){
                break;
            }
        }
    }
}

输出结果:

out

六、快速排序

思想:

通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序的目的。
分析

步骤总结:

(1)选择枢轴(基准、pivot)

(2)分割数据
① 从后面序列找比枢轴小的放到枢轴前面
② 从前面序列找枢比轴大的放到枢轴后面
通过枢轴分割数据,使得枢轴左边的值比枢轴小,枢轴右边的值比枢轴大

(3)重复递归被枢轴分割的数据,可以先处理左边的数据,再处理右边的数据

1. 快速排序递归写法

/**
 * 快速排序
 * 递归写法
 * 最坏时间复杂度:O(n^2), 最好时间复杂度:O(n*logn)[刚好平均分割整个数据]
 * 最坏空间复杂度:O(n), 最好空间复杂的:O(logn)
 * 稳定性:不稳定
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {5,2,3,9,4,7};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        int start = 0;
        int end = arr.length-1;
        quickSort(arr,start,end);
        System.out.println(Arrays.toString(arr));
    }

    public static void quickSort (int[] arr, int start, int end){
        if(start >= end){ //边界条件
            return;
        }
        int i = start;
        int j = end;
        int temp = arr[i];
        while (i < j){
            //1. 从后面序列找比枢轴小的放到枢轴前面
            //whille 循环中的"="防止死循环重复赋值, i<j 防止此次循环数组越界
            while (i < j && arr[j] >= temp){
                j--;
            }
            arr[i] = arr[j];

            //2. 从前面序列找比枢轴大的放到枢轴后面
            while (i < j && arr[i] <= temp){
                i++;
            }
            arr[j] = arr[i];
        }
        arr[i] = temp;//填上缺口
        
        int pivot = i; //拿到每次新的枢轴

        //3. 分别递归求左右两边的序列
        quickSort(arr,start,pivot-1);
        quickSort(arr,pivot+1,end);

    }
}

输出结果:

out

图解分析:

1
2

2. 优化快速排序

(1)随机选取基准
随机选取基准的时候,会带来不确定的因素,很有可能优化不成功。

(2)使用三数取中法来确定分割的基准,一般取左端、右端及中间的三个数,而这在这三个数中,中间大小的数被确定为基准。比方说:[ 3 1 5 ],我们取 3,因为最小数为 1,最大数为 5。这样可以避免两种极端的情况:

① 当最大或最小的数分布在数据的两边
② 当数据整体有序的情况

(3) 将和基准相同的元素,从分散的位置聚集到一起,这样会使递归的次数减少,从而使程序运行速度更快。

out

(4)当分割一定的长度后,使用直接插入排序。

3. 三数取中法优化

/**
 * 快速排序优化
 * 三数取中法
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {6,1,2,7,9,3,4,5,10,8};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        //int[] arr = {5,2,3,9,4,7};
        quickSort(arr,0,arr.length-1);
        System.out.println(Arrays.toString(arr));
    }

    public static void quickSort(int[] arr, int start, int end){
        if(start >= end){
            return;
        }
        //三数取中法,防止取到顺序/逆序的情况
        int midValIndex = findMidValIndex(arr, start, end); 

        //交换基准
        int temp = arr[start];
        arr[start] = arr[midValIndex];
        arr[midValIndex] = temp;

        int base = partition(arr,start,end); //接收基准
        quickSort(arr,start,base-1);
        quickSort(arr,base+1,end);
    }

	//找中间值
    public static int findMidValIndex(int[] arr, int start, int end){
        int mid = (start+end) / 2;
        if(arr[start] < arr[end]){
            if(arr[mid] < arr[start]){
                return start;
            }else if(arr[mid] > arr[end]){
                return end;
            }else {
                return mid;
            }
        }else {
            if(arr[mid] < arr[end]){
                return end;
            }else if(arr[mid] > arr[start]){
                return start;
            }else {
                return mid;
            }
        }
    }

    //寻找基准
    public static int partition(int[] arr, int start, int end){
        int temp = arr[start];
        while(start < end){
            //在数据的后半部分寻找比基准小的
            while(start < end && arr[end] >= temp){ // “=” 防止死循环,防止重复赋值
                end--;
            }
            arr[start] = arr[end];
            //在数据的前半部分寻找比基准大的
            while(start < end && arr[start] <= temp){
                start++;
            }
            arr[end] = arr[start];
        }
        arr[start] = temp;
        return start;
    }
}

4. 快速排序的非递归写法

/**
 * 快速排序
 * 非递归写法
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {5,2,3,9,4,7};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        int start = 0;
        int end = arr.length-1;
        quickSort(arr,start,end);
        System.out.println(Arrays.toString(arr));
    }

    public static void quickSort(int[] arr, int start, int end){
        Stack<Integer> stack = new Stack<>();
        int pivot = partition(arr,start,end);

        if(start+1 < pivot){
            stack.push(start);
            stack.push(pivot-1);
        }

        if(pivot < end-1){
            stack.push(pivot+1);
            stack.push(end);
        }

        while (!stack.isEmpty()){
            end = stack.pop();
            start = stack.pop();

            pivot = partition(arr,start,end);

            if(start+1 < pivot){
                stack.push(start);
                stack.push(pivot-1);
            }
            
            if(pivot < end-1){
                stack.push(pivot+1);
                stack.push(end);
            }
        }

    }

    public static int partition (int[] arr, int i, int j){
        int temp = arr[i];
        while (i < j){
            //1. 从后面序列找比枢轴小的放到枢轴前面
            //"="防止死循环重复赋值, i<j 防止此次循环数组越界
            while (i < j && arr[j] >= temp){
                j--;
            }
            arr[i] = arr[j];

            //2. 从前面序列找枢比轴大的放到枢轴后面
            while (i < j && arr[i] <= temp){
                i++;
            }
            arr[j] = arr[i];
        }
        arr[i] = temp;
        return i;//拿到每次的枢轴
    }
}

七、归并排序

1. 合并两个有序数组

理解归并排序之前,一定要知道两个已经有序的数组是怎么合并的,并理解它们的思想。

思想:

① 定义 s1 为 arr1 数组的初始下标,e1 为 arr1 数组的末尾下标。
② 定义 s2 为 arr2 数组的初始下标,e2 为 arr2 数组的末尾下标。
③ 将 s1 和 s2 进行比较,较小者放入新的数组中,紧着,让 s1 / s2 进行遍历,之后循环重复实现以上的操作,直到有一个数组遍历完,就可以将另一个未遍历完的数组中的剩下元素放入新数组的末尾。

out

代码较为简单,不再赘述。

/**
 * 合并两个有序的数组
 */
public class Test {
    public static void main(String[] args) {
        int[] arr1 = {1,6,7,9};
        int[] arr2 = {2,3,5,8};
        int[] arr = merge(arr1,arr2);
        System.out.println(Arrays.toString(arr));
    }

    public static int[] merge(int[] arr1, int[] arr2){
        int[] arr = new int[arr1.length+arr2.length];
        int s1 = 0;
        int e1 = arr1.length-1;
        int s2 = 0;
        int e2 = arr2.length-1;
        int i = 0;
        while (s1 <= e1 && s2 <= e2){
            if(arr1[s1] < arr2[s2]){
                arr[i++] = arr1[s1++];
            }else {
                arr[i++] = arr2[s2++];
            }
        }
        while (s1 <= e1){
            arr[i++] = arr1[s1++];
        }
        while (s2 <= e2){
            arr[i++] = arr2[s2++];
        }
        return arr;
    }
}

输出结果:

out
现在我们来看一下归并排序的思想:

归并排序就是先在一个待排序数组中选择一个中间下标对应元素(并不是大小为中间元素),通过这个元素分割成两个逻辑上的数组,在这两个数组中,再寻找中间对应元素,之后不断分割,直至某个数组无法再分。这样一来,就可以对某一个数组进行有序排序,之后再进行整合,而这里每次整合过后,都要回归原始数组中。

在下图分析中,拆就是递的过程,合就是归的过程,所以这体现了递归的思想。
分析

2. 归并排序递归写法

/**
 * 归并排序
 * 时间复杂度:O(n*logn)
 * 空间复杂度:O(n)
 * 稳定性:稳定
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {9,6,7,1,3,8,5,2};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        int low = 0;
        int high = arr.length-1;
        mergeSort(arr,low,high);
        System.out.println(Arrays.toString(arr));
    }
    
    public static void mergeSort(int[] arr, int low, int high){
        if(low == high){
            return;
        }
        int mid = (low+high) / 2;
        
        mergeSort(arr,low,mid); //先拆左边
        mergeSort(arr,mid+1,high); //再拆右边

        merge(arr,low,mid,high); //拆完过后,开始合并
    }

    //合并
    public static void merge(int[] arr, int low, int mid, int high){
        int[] temp = new int[high-low+1];
        int i = 0;
        int s1 = low;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = high;

        while (s1 <= e1 && s2 <= e2){
            if(arr[s1] < arr[s2]){
                temp[i++] = arr[s1++];
            }else {
                temp[i++] = arr[s2++];
            }
        }
        while (s1 <= e1){
            temp[i++] = arr[s1++];
        }
        while (s2 <= e2){
            temp[i++] = arr[s2++];
        }

        //将 temp 中的数组拷贝至原数组中
        for (int j = 0; j < i; j++) {
            arr[j+low] = temp[j]; //”j+low“ 以防对原数组进行覆盖
        }
    }
}

输出结果:

out

3. 归并排序非递归写法

/**
 * 归并排序
 * 非递归写法
 */
public class Test {
    public static void main(String[] args) {
        int[] arr = {9,6,7,1,3,8,5,2};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void mergeSort(int[] arr){
        int size = 1; // 第一次归并两个元素,第二次归并四个元素...
        while (size < arr.length){

            for (int i = 0; i < arr.length; i += size*2) {
                int low = i;
                int mid = low+size-1;

                if(mid >= arr.length){
                    mid = arr.length-1;
                }

                int high = mid+size;
                if(high >= arr.length){
                    high = arr.length-1;
                }

                merge(arr,low,mid,high);
            }
            size *= 2;
        }
    }

    public static void merge(int[] arr, int low, int mid, int high){
        int[] ret = new int[high-low+1];
        int s1 = low;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = high;
        int i = 0;
        while (s1 <= e1 && s2 <= e2){
            if(arr[s1] < arr[s2]){
                ret[i++] = arr[s1++];
            }else {
                ret[i++] = arr[s2++];
            }
        }
        while (s1 <= e1){
            ret[i++] = arr[s1++];
        }
        while (s2 <= e2){
            ret[i++] = arr[s2++];
        }

        for (int j = 0; j < i; j++) {
            arr[j+low] = ret[j];
        }
    }
}

输出结果:

out

八、了解三个不需要比较数据就能进行排序的方法

1. 基数排序

说明:可以将下面装数据的容器想象成队列的结构,而不是栈。
下图只是为了方便演示,所以才会画成这个容器哦。
analysis

2. 计数排序

分析

3. 桶排序

分析

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十七ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值