插入排序、冒泡排序、选择排序、希尔排序、堆排序、快速排序、归并排序

本文详细介绍了排序算法的几种常见类型,包括冒泡排序、插入排序、选择排序、希尔排序、堆排序、快速排序和归并排序,分析了它们的时间复杂度、空间复杂度和稳定性,并提供了具体的实现示例。
摘要由CSDN通过智能技术生成

1.  排序

(1)前提是存储结构是线性表(一般都是顺序表 ,甚至以数组为主)

(2)要求元素是具备比较大小的能力(Java中:基本类型中的数值、对象+Comparable、对象+Comprartor)

(3)原地(就地)排序:在给定数组的原有空间中完成排序,结果在原数组中体现即可。

(4)数据全在内存中的排序(数组+内存:取下标访问元素的时间复杂度可认为一定是O(1) )。

(5)排序代码中的关键:区间:数组+开始位置下标+结束位置下标,左闭右开[  )、左闭右闭[  ]。

(6)常见的排序算法

(7)O(n^2)级别的算法:冒泡排序、选择排序、插入排序

PS:减治算法(分治算法):

1)减:通过一个有限步骤,使得问题的规模变小。冒泡排序:冒泡步骤;选择排序:选择+交换;插入排序:查找+插入。把n个数的排序变为(n-c)个数的排序。

2)治:剩余的问题还是使用相同的方式进行解决。出口:待排序数组的长度<=1时。

2. 插入排序(Insert Sort)

(1)在有序区间中,找到合适的位置并插入。

 (2)遍历方式

   从后往前遍历有序区间,每个元素即为e,要插入的元素记为x(一定紧挨着有序区间,且在后面).遍历完后,要么e<=x,x应插入在e后边(紧挨着);要么整个区间都遍历完了,x应插入在第一个[0]。期间为保证插入x时位置存在,当e>x时,把e往后移给x腾地。

public static void insertSort(long[] arr){
        for(int i=0;i<arr.length;i++){
            long x=arr[i];
            int j;
            for(j=i-1;j>=0 && arr[j]>x;j--){
                arr[j+1]=arr[j];
            }
            arr[j+1]=x;
        }

(3)时间与空间复杂度

1)时间复杂度

最好:每个x都从小到大已经有序,O(n)

最坏:x比有序区间的最后一个元素还小(待排序的原数组是从大到小逆序排列),O(n^2)

平均:O(n^2)

2)空间复杂度

都是O(1),是一种稳定的排序算法。

3. 冒泡排序(Bubble Sort)

(1)冒泡排序:通过冒泡的过程,把所有无序区间的所有元素都消除掉(剩下一个就实际有序了)。

把整个数组看作前边是无序的、后边是有序的。

每次冒泡过程:在无序区间相邻的两个元素间比较,确保无序区间的所有元素都参加了比较。目标是当前无序区间的最大值冒泡到当前无序区间的最后一个位置。无序区间少了一个元素,有序区间多了一个元素。

整个冒泡排序有  n-1  次冒泡过程。

(2)冒泡过程中一旦发生了交换,说明存在相邻两元素之间  前边的元素>后边的元素。反过来说,单次冒泡过程,若一次交换都没有发生,说明不存在相邻两元素之间  前边的元素>后边的元素。

任意相邻的两个元素之间,前边的元素<=后边的元素。

冒泡过程只在无序区间进行。无序区间的任意相邻两个元素满足  前边的元素<=后边的元素。我们认为的无序区间是有序的。所以整个数组已经全是有序的。

public static void BubbleSort(long[] arr){
        for(int i=0;i< arr.length-1;i++){//需要n-1次冒泡过程
            
            boolean sorted=true;//每次冒泡过程,假设无序区间是有序的
            for(int j=0;j< arr.length-i-1;j++){
                if(arr[j]>arr[j+1]){//这里若加了=就事情了稳定性
                    swap(arr,j,j+1);
                    sorted=false;//说明假设无序区间是有序的不成立
                }
            }
            if(sorted){//冒泡过程完成后,若sorted==true,说明一次交换过程都没发生过。
                return;
            }
        }
    }

(3)复杂度

1)时间复杂度

最好:O(n),当数组已经有序。

最坏:O(n^2),当数组是逆序的。

平均:O(n^2)

2)空间复杂度

都是O(1),具备稳定性

PS:(24条消息) 排序算法——冒泡排序VS插入排序_LemmonTreelss的博客-CSDN博客_冒泡排序 插入排序

4. 选择排序(Select Sort)

(1)把整个待排序数组看作两个区间:无序区间+有序区间

1)[无序区间] [有序区间]  找到无序区间中最大元素所在的下标,然后将该元素放到无序区间最后。如:[5 3 2 4 2][6 7 8 9]--> [3 2 4 2][5 6 7 8 9]

2)[有序区间] [无序区间]   找到无序区间中最下元素所在的下标,然后将该元素放到无序区间开始。如:[0 1 2 3][9 4 5 8]-->[0 1 2 3 4][9 5 8]

(2)在无序区间中找到最大元素:暴力查找(遍历)

public static void SelectSort(long[] arr){
        for(int i=0;i<arr.length-1;i++){
            //无序区间+有序区间  -->  [0,arr.length-i)[arr.length-i,arr.length)
            int maxIdx=0;
            for(int j=1;j<arr.length-i;j++){
                if(arr[j]>arr[maxIdx])
                    maxIdx=j;
            }
            //无序区间的最大元素为arr[maxIdx],把它放到无序区间最后一个位置,无序区间最后一个元素位置下标是[arr.length-i-1]
            swap(arr,maxIdx, arr.length-i-1);
        }
    }
    public static void swap(long[] arr,int i,int j){
        long temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

(3)在无序区间中找到最小元素

public static  void SelectSort1(long[] arr){
        for(int i=0;i< arr.length-1;i++){
            //有序区间+无序区间--》找到无序区间中最小元素所在的下标,然后将该元素放到无序区间开始
            int minIdx=i;
            for(int j=i+1;j< arr.length;j++){
                if(arr[j]<arr[minIdx])
                    minIdx=j;
            }
            swap(arr,minIdx,i);
        }
    }

(4)复杂度

1)时间复杂度

最好、最坏、平均:O(n^2)

2)空间复杂度

都是O(1),但保证不了稳定性

5. 希尔排序(Shell Sort)

(1)希尔排序

希尔排序是插入排序的升级版本。

从无序区间(带间隔的)中取第一个元素,去有序区间(带间隔的)进行查找并插入。

PS:插入排序,元素越趋于有序,效率越高。希尔排序,进行大量的分组插排。

(2)在这个过程中我们选择增量 gap=length/2 ,缩小增量以 gap=gap/2 的方式。

public static void ShellSort(long[] arr){
        if(arr.length==0)
            return;
        int gap=arr.length/2;
        while(true){
            for(int i=gap;i<arr.length;i++){
                long x=arr[i];
                int j;
                for(j=i-gap;j>=0 && arr[j]>x;j-=gap){
                    arr[j+gap]=arr[j];
                }
                arr[j+gap]=x;
            }
            if(gap==1)
                break;
            gap=gap/2;
        }
    }

PS:希尔排序相关:        百度安全验证https://baijiahao.baidu.com/s?id=1707870928646769729&wfr=spider&for=pc

6. 堆排序(Heap Sort)

选择排序的优化-->堆排序

[无序区间] [有序区间] 中,将无序区间维护成一个大堆(从小到大排序,需要大堆),利用大堆从无序区间中找到最大值,交换。

PS:可参考:http://t.csdn.cn/adOpq

7. 快速排序(Quick Sort)

1)思想:对一个区间的元素进行排序,在这个区间中任选一个元素(一般挑区间最边上的元素--最右边的, pivot ),遍历整个待排序区间,将每个元素都和 pivot 作比较(<=或>=),同时把 pivot 放到合适的位置,要求所有(<= pivot )的元素都在 pivot 左边,所有(>= pivot )的元素都在 pivot 右边。

(2)基本框架

在待排序区间内,选择一个基准值 pivot ;

对待排序区间进行 partition 操作,目标是将待排序区间分开,左边的<= pivot ,右边的>= pivot ;

分别在对左右两个小区间按照相同方式进行处理,知道待排序区间元素个数 <=1 ,此时待排序区间就是有序的。

PS:分治算法:把一个大问题(n个元素的排序问题)经过一定的步骤(partition)分成多个(至少两个)相同的小规模问题,再按照相同方式继续处理小问题。

(3)具体 partition 的方式

PS:一定要确保 partition 的区间内的每个元素都和 pivot 做过比较 。

1)hoare 法:左右往中间靠

 private static int partitionHoare(long[] array, int from, int to) {
            long pivot = array[to]; // 选择区间的最右边的元素作为基准值
            int left = from;
            int right = to;

            // 当 [left, right) 区间内还有元素时,循环,得继续
            while (left < right) {  // left < right 只在此处判断了
                // 当选择最右边作为基准值时,优先从左边开始,否则,某些情况下会有元素没有和 pivot 比较过
                while (left < right && array[left] <= pivot) {
                    left++;     // left 一直在变,可能破坏 left < right 的约束
                }

                while (left < right && array[right] >= pivot) {
                    right--;    // right 一直在变,可能破坏 left < right 的约束
                }

                // array[left] > pivot && array[right] < pivot
                swap(array, left, right);
            }

            /* 真实中的快排不用写这些代码的,只是为了检查正确性约束 */
            assertTrue(left == right, "left 和 right 必须相等");
            assertTrue(left >= from && left <= to, "left 必须在 [from, to] 区间内");

            // 断言 left == right
            // right 的位置就是 pivot 应该所在的下标
            // [right, to] 大于等于基准值
            // 即使交换 [right] 和 [to] 也没有打破 [right, to] 大于等于基准值
            swap(array, right, to);

            // [from, left) 都是小于等于基准值的
            for (int i = from; i < left; i++) {
                assertTrue(array[i] <= pivot, "左边区间的元素必须都是小于等于基准值");
            }
            // [left + 1, to] 都是大于等于基准值的
            for (int i = left + 1; i <= to; i++) {
                assertTrue(array[i] >= pivot, "右边区间的元素都是大于等于基准值");
            }

            return right;   // 最终返回基准值所在的下标
        }

        private static void swap(long[] array, int i, int j) {
            long t = array[i];
            array[i] = array[j];
            array[j] = t;
        }

        private static void assertTrue(boolean condition, String message) {
            if (!condition) {
                throw new RuntimeException(message);
            }
        }

2)挖坑法:左右往中间靠

 private static int partitionDigHole(long[] array, int from, int to) {
            long pivot = array[to];
            int left = from;
            int right = to;


            // 当 [left, right) 区间内还有元素时,循环,得继续
            while (left < right) {
                while (left < right && array[left] <= pivot) {
                    left++;
                }

                array[right] = array[left];

                while (left < right && array[right] >= pivot) {
                    right--;
                }

                array[left] = array[right];
            }

            /* 真实中的快排不用写这些代码的,只是为了检查正确性约束 */
            assertTrue(left == right, "left 和 right 必须相等");
            assertTrue(left >= from && left <= to, "left 必须在 [from, to] 区间内");
            // [from, left) 都是小于等于基准值的
            for (int i = from; i < left; i++) {
                assertTrue(array[i] <= pivot, "左边区间的元素必须都是小于等于基准值");
            }
            // [left + 1, to] 都是大于等于基准值的
            for (int i = left + 1; i <= to; i++) {
                assertTrue(array[i] >= pivot, "右边区间的元素都是大于等于基准值");
            }

            array[left] = pivot;

            return left;
        }

3)前后指针法:从前往后靠

private static int partition3(long[] array, int from, int to) {
        /*
        小于基准值的区间范围:       [from, b)
        大于等于基准值的区间范围:[b, d)
        未比较的元素的区间范围:   [d, to)
        基准值                                 [to, to]
         */

            long pivot = array[to];
            // 最开始
            // [from, b) => [from, from)  区间内一个元素都没有
            // [b, d) => [from, from) 区间内一个元素都没有
            // [d, to) => [from, to)  整个区间所有元素都在里面
            int b = from;
            for (int d = from; d < to; d++) {
                if (array[d] < pivot) {
                    swap(array, b, d);
                    b++;
                }
            }

            swap(array, b, to);

            return b;
        }

4)进阶版本:

public static void 进阶版Partition(long[] array, int from, int to) {
            long pivot = array[to];
        /*
        [from, b)  小于基准值
        [b, d)     等于基准值
        [d, g]     未比较的元素
        (g, to]    大于基准值
         */

            int b = from;
            int d = from;
            int g = to;

            // [d, g] 这个区间内没有元素时停止; d <= g 说明还有元素
            while (d <= g) {
                if (array[d] == pivot) {
                    d++;
                } else if (array[d] < pivot) {
                    swap(array, d, b);
                    b++;
                    d++;
                } else {
                    // array[d] > pivot
                    swap(array, d, g);
                    g--;
                }
            }
        }

(4)复杂度

1)时间复杂度

最好:O(n*log(n)) ,共有n层,每层基本是O(n) 。

最坏:O(n^2)

平均:O(n*log(n))

2)空间复杂度

最好:O(log(n)),完全二叉树的高度

最坏:O(n),单枝树的高度

平均:O(log(n))

(5)快速排序的优化

1)处理数据量较小的排序时,插排的速度更优

2)更细致的选择基准值:再待排序区间中选择一个随机位置作为基准值(计算机生成随机数速度不快),再把选出来的基准值交换到最右边。如:

private static void quickSortInternal(long[] array, int from, int to) {
        // 区间内的元素个数
        int size = to - from + 1;
        if (size <= 16) {
            insertSort(array, from, to);
            return;
        }

        // 三数取中法
        long leftValue = array[from];
        long rightValue = array[to];
        int midIdx = from + (to - from) / 2;
        long midValue = array[midIdx];

        if (leftValue <= rightValue) {
            if (rightValue <= midValue) {
                // rightValue 作为基准值
            } else if (leftValue <= midValue) {
                // midValue
                swap(array, midIdx, to);
            } else {
                // leftValue
                swap(array, from, to);
            }
        } else {
            // leftValue > rightValue
            if (leftValue <= midValue) {
                // leftValue
                swap(array, from, to);
            } else if (midValue <= rightValue) {
                // rightValue
            } else {
                // midValue
                swap(array, midIdx, to);
            }
        }

        int i = partition(array, from, to);

        quickSortInternal(array, from, i - 1);
        quickSortInternal(array, i + 1, to);
    }

    private static int partition(long[] array, int from, int to) {
        long pivot = array[to];
        int left = from;
        int right = to;

        while (left < right) {
            while (left < right && array[left] <= pivot) {
                left++;
            }

            array[right] = array[left];

            while (left < right && array[right] >= pivot) {
                right--;
            }

            array[left] = array[right];
        }

        array[left] = pivot;

        return left;
    }

    private static void swap(long[] array, int i, int j) {
        long t = array[i];
        array[i] = array[j];
        array[j] = t;
    }

    private static void insertSort(long[] array, int from, int to) {
        int size = to - from + 1;
        for (int i = 1; i < size; i++) {
            // 有序区间 [from, from + i)
            // 无序区间的第一个元素的下标 [from + i]
            long x = array[from + i];

            // 遍历有序区间
            int j;
            for (j = from + i - 1; j >= from && array[j] > x; j--) {
                array[j + 1] = array[j];
            }

            array[j + 1] = x;
        }
    }

(6)非递归实现快速排序

public static void quickSort非递归(long[] array) {
        Deque<Integer> stack = new LinkedList<>();
        // 一开始就把待排序区间放入
        stack.push(0);
        stack.push(array.length - 1);

        while (!stack.isEmpty()) {
            int to = stack.pop();
            int from = stack.pop();
            if (to - from + 1 <= 1) {
                continue;
            }

            int i = partition(array, from, to);

            stack.push(from);
            stack.push(i - 1);

            stack.push(i + 1);
            stack.push(to);
        }
    }
    private static int partition(long[] array, int from, int to) {
        long pivot = array[to];
        int left = from;
        int right = to;

        while (left < right) {
            while (left < right && array[left] <= pivot) {
                left++;
            }

            array[right] = array[left];

            while (left < right && array[right] >= pivot) {
                right--;
            }

            array[left] = array[right];
        }

        array[left] = pivot;

        return left;
    }

8. 归并排序(Merge Sort)

(1)复杂度

1)时间复杂度

最好、最坏、平均:O(n*log(n)) 

2)空间复杂度

最好、最坏、平均:O(n),具备稳定性

(2)实现

1)

 public static void mergeSort(long[] array) {
        mergeSortInternal(array, 0, array.length);
    }

    // 待排序区间:[from, to)
    private static void mergeSortInternal(long[] array, int from, int to) {
        if (to <= 1 + from) {
            return;
        }

        // 把待排序区间对半分开;找到中间位置下标
        int mid = from + (to - from) / 2;
        // 整个区间被切割成, [from, mid), [mid, to),按照相同的方式,先把左右两个区间变得有序
        mergeSortInternal(array, from, mid);
        mergeSortInternal(array, mid, to);

        // [from, mid)   有序了
        // [mid, to)     有序了

        merge(array, from, mid, to);
    }

    private static void merge(long[] array, int from, int mid, int to) {
        int size = to - from;
        // 需要的额外空间
        long[] e = new long[size];
        int eIdx = 0;

        int leftIdx = from; // 左边区间要处理的元素的下标
        int rightIdx = mid; // 右边区间要处理的元素的下标
        // 什么条件表示左边区间 [from, mid) 就没有元素了   leftIdx == mid
        // 同理右边区间:rightIdx == to
        // 两个区间都有元素的时候才要比较
        // 左区间有元素:leftIdx < mid
        // 右: rightIdx < to
        // leftIdx < mid && rightIdx < to
        while (leftIdx < mid && rightIdx < to) {
            // 要比较的两个元素 array[leftIdx] 和 array[rightIdx]
            if (array[leftIdx] <= array[rightIdx]) {
                // 取左边的元素放入 e 数组
                e[eIdx] = array[leftIdx];
                eIdx++;
                leftIdx++;
            } else {
                // 取右边的元素放入 e 数组
                e[eIdx] = array[rightIdx];
                eIdx++;
                rightIdx++;
            }
        }

        // 说明有一个区间的元素被全部取完了
        if (leftIdx < mid) {
            // 右边取完了,把左边的所有元素依次放到 e 中
            while (leftIdx < mid) {
                e[eIdx] = array[leftIdx];
                eIdx++;
                leftIdx++;
            }
        } else {
            while (rightIdx < to) {
                e[eIdx] = array[rightIdx];
                eIdx++;
                rightIdx++;
            }
        }

        // e 里是有序的 [0, size)
        // 要把有序的元素从 e 中搬回到 array 中 [from, to)

        for (int i = 0; i < size; i++) {
            array[from + i] = e[i];
        }
    }

2)

public static void mergeSort2(long[] array) {
        long[] e = new long[array.length];
        mergeSortInternal2(array, 0, array.length, e);
    }
    private static void mergeSortInternal2(long[] array, int from, int to, long[] e) {
        if (to <= 1 + from) {
            return;
        }

        // 把待排序区间对半分开
        // 找到中间位置下标
        int mid = from + (to - from) / 2;
        // 整个区间被切割成
        // [from, mid)
        // [mid, to)
        // 按照相同的方式,先把左右两个区间变得有序
        mergeSortInternal2(array, from, mid, e);
        mergeSortInternal2(array, mid, to, e);

        // [from, mid)   有序了
        // [mid, to)     有序了

        merge2(array, from, mid, to, e);
    }

    private static void merge2(long[] array, int from, int mid, int to, long[] e) {
        int size = to - from;
        int eIdx = 0;

        int leftIdx = from; // 左边区间要处理的元素的下标
        int rightIdx = mid; // 右边区间要处理的元素的下标
        // 什么条件表示左边区间 [from, mid) 就没有元素了   leftIdx == mid
        // 同理右边区间:rightIdx == to
        // 两个区间都有元素的时候才要比较
        // 左区间有元素:leftIdx < mid
        // 右: rightIdx < to
        // leftIdx < mid && rightIdx < to
        while (leftIdx < mid && rightIdx < to) {
            // 要比较的两个元素 array[leftIdx] 和 array[rightIdx]
            if (array[leftIdx] <= array[rightIdx]) {
                // 取左边的元素放入 e 数组
                e[eIdx] = array[leftIdx];
                eIdx++;
                leftIdx++;
            } else {
                // 取右边的元素放入 e 数组
                e[eIdx] = array[rightIdx];
                eIdx++;
                rightIdx++;
            }
        }

        // 说明有一个区间的元素被全部取完了
        if (leftIdx < mid) {
            // 右边取完了,把左边的所有元素依次放到 e 中
            while (leftIdx < mid) {
                e[eIdx] = array[leftIdx];
                eIdx++;
                leftIdx++;
            }
        } else {
            while (rightIdx < to) {
                e[eIdx] = array[rightIdx];
                eIdx++;
                rightIdx++;
            }
        }

        // e 里是有序的 [0, size)
        // 要把有序的元素从 e 中搬回到 array 中 [from, to)

        for (int i = 0; i < size; i++) {
            array[from + i] = e[i];
        }
    }

3)


    public static void mergeSort非递归版本(long[] array) {
        long[] e = new long[array.length];

        // i 个元素和 i 个元素要归并
        for (int i = 1; i < array.length; i *= 2) { // Log(n)
            // 内部的循环代表,代表一组一组完成归并
            for (int j = 0; j < array.length; j += (2 * i)) {   // O(n)
                int from = j;
                int mid = from + i;
                if (mid >= array.length) {
                    // 说明没有右边的区间,本次不需要处理了
                    break;
                }
                int to = mid + i;
                if (to > array.length) {
                    to = array.length;
                }

                merge2(array, from, mid, to, e);
            }
        }
    }
    private static void merge2(long[] array, int from, int mid, int to, long[] e) {
        int size = to - from;
        int eIdx = 0;

        int leftIdx = from; // 左边区间要处理的元素的下标
        int rightIdx = mid; // 右边区间要处理的元素的下标
        // 什么条件表示左边区间 [from, mid) 就没有元素了   leftIdx == mid
        // 同理右边区间:rightIdx == to// 两个区间都有元素的时候才要比较// 左区间有元素:leftIdx < mid
        // 右: rightIdx < to// leftIdx < mid && rightIdx < to
        
        while (leftIdx < mid && rightIdx < to) {
            // 要比较的两个元素 array[leftIdx] 和 array[rightIdx]
            if (array[leftIdx] <= array[rightIdx]) {
                // 取左边的元素放入 e 数组
                e[eIdx] = array[leftIdx];
                eIdx++;
                leftIdx++;
            } else {// 取右边的元素放入 e 数组
                e[eIdx] = array[rightIdx];
                eIdx++;
                rightIdx++;
            }
        }

        // 说明有一个区间的元素被全部取完了
        if (leftIdx < mid) {
            // 右边取完了,把左边的所有元素依次放到 e 中
            while (leftIdx < mid) {
                e[eIdx] = array[leftIdx];
                eIdx++;
                leftIdx++;
            }
        } else {
            while (rightIdx < to) {
                e[eIdx] = array[rightIdx];
                eIdx++;
                rightIdx++;
            }
        }

        // e 里是有序的 [0, size)
        // 要把有序的元素从 e 中搬回到 array 中 [from, to)

        for (int i = 0; i < size; i++) {
            array[from + i] = e[i];
        }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值