排序算法

排序算法

排序算法是属于《数据结构和算法》中最基本的算法只之一,今天我们使用 Java 语言来实现常用的排序算法,下面的代码全部以升序为例。

目录

排序算法

插入排序

思路

代码实现

复杂度与稳定性

特点

希尔排序

思路

代码

复杂度和稳定性

选择排序

思路

代码

复杂度和稳定性

堆排序

思路

代码

复杂度和稳定性

冒泡排序

思路

代码

复杂度和稳定性

快速排序

思路

代码

复杂度和稳定性

归并排序

思路

代码

复杂度和稳定性


插入排序

思路

插入排序的思路是:先将待排序序列分成两个部分,前一部分为已排序序列,剩余部分为未排序序列,然后从未排序的部分中选择第一个元素,在已排序序列中选择合适的位置插入。

代码实现

    public static void insertSort(int[] arr) {
        int bound = 1;
        // [0, bound) 是已排序区间.
        // [bound, length) 待排序区间.
        for (; bound < arr.length; bound++) {
            int v = arr[bound];
            int cur = bound - 1;
            for (; cur >= 0; cur--) {
                if (arr[cur] > v) {
                    arr[cur + 1] = arr[cur];
                } else {
                    break;
                }
            }
            arr[cur + 1] = v;
        }
    }

复杂度与稳定性

最好时间复杂度为 O(N),发生在数据已经是升序的情况

最坏时间复杂度为 O(N^2),发生在数据是逆序的情况

平均时间复杂度为 O(N^2)

空间复杂度为 O(1)

是稳定的。

特点

数据越有序排序的效率越高,数据越短排序的效率越高。

希尔排序

思路

希尔排序是插入排序的优化,由于插入排序有数据越有序排序的效率越高,数据越短排序的效率越高的特点,所以将数据分组,对每一组进行插入排序。

代码

    // 谢尔/希尔
    public static void shellSort(int[] arr) {
        // 指定 gap 序列, len/2, len/4, len/8,...,1
        int gap = arr.length / 2;
        while (gap >= 1) {
            _shellSort(arr, gap);
            gap = gap / 2;
        }
    }

    public static void _shellSort(int[] arr, int gap) {
        // 进行分组插排. 分组依据就是 gap.
        // gap 同时也表示分的组数.
        // 同组的相邻元素, 下标差值就是 gap
        // 下面的代码其实和插入排序是一样的. 尤其是把 gap 设为 1
        int bound = gap;
        for (; bound < arr.length; bound++) {
            int v = arr[bound];
            int cur = bound - gap;
            for (; cur >= 0; cur -= gap) {
                if (arr[cur] > v) {
                    // 进行搬运
                    arr[cur + gap] = arr[cur];
                } else {
                    break;
                }
            }
            arr[cur + gap] = v;
        }
    }

注意分组是根据 gap 来分的。

复杂度和稳定性

最好时间复杂度 O(N)

最坏时间复杂度 O(N^2)

平均时间复杂度 O(N^1.3)

空间复杂度 O(1)

不稳定的。

选择排序

思路

选择排序是一种打擂台的思想,每一次从无序区间选出最大(或最小)的一个元素,存放在无序区间的最后(或最前),直到全部待排序的数据元素排完。

代码

    public static void selectSort(int[] arr) {
        // 创建一个变量 bound 表示已排序区间和待排序区间的边界.
        // [0, bound) 已排序区间
        // [bound, length) 待排序区间
        int bound = 0;
        for (; bound < arr.length; bound++) {
            // 里层循环要进行打擂台的过程.
            // 擂台的位置就是 bound 下标的位置
            for (int cur = bound + 1; cur < arr.length; cur++) {
                if (arr[cur] < arr[bound]) {
                    // 如果发现挑战者比擂主小, 就交换两个元素
                    swap(arr, cur, bound);
                }
            }
        }
    }

    private static void swap(int[] arr, int index1, int index2) {
        int tmp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = tmp;
    }

复杂度和稳定性

时间复杂度 O(N^2)

空间复杂度 O(1)

 不稳定的

堆排序

思路

堆排序借助堆,先将元素建立成一个大堆,取出堆顶元素,将堆顶元素和数组末尾交换,此时末尾就是最大值,将堆的元素个数减一,从 0 开始向下调整,调整完毕后取出堆顶元素,放在数组的倒数第二个位置,重复此步骤,直到整个数组有序。

代码

    public static void heapSort(int[] arr) {
        // 1. 先建立堆!!
        createHeap(arr);
        // 2. 需要循环的取出堆顶元素, 和最后一个元素交换并删除之
        //    再从 0 位置进行调整
        int heapSize = arr.length;
        for (int i = 0; i < arr.length; i++) {
            // 交换 0 号元素和堆的最后一个元素
            swap(arr, 0, heapSize - 1);
            // 把最后一个元素从堆上删除
            heapSize--;
            // 从 0 号位置开始往下进行调整
            shiftDown(arr, heapSize, 0);
        }
    }

    public static void createHeap(int[] arr) {
        for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
            shiftDown(arr, arr.length, i);
        }
    }

    public static void shiftDown(int[] arr, int size, int index) {
        int parent = index;
        int child = 2 * parent + 1;
        while (child < size) {
            // 先找出左右子树比较大的~
            if (child + 1 < size && arr[child + 1] > arr[child]) {
                child = child + 1;
            }
            // 再去比较 child 和 parent
            if (arr[parent] < arr[child]) {
                swap(arr, parent, child);
            } else {
                break;
            }
            parent = child;
            child = 2 * parent + 1;
        }
    }

复杂度和稳定性

时间复杂度 O(N*logN)

空间复杂度 O(1)

不稳定

冒泡排序

思路

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

代码

    public static void bubbleSort(int[] arr) {
        int bound = 0;
        for (; bound < arr.length; bound++) {
            for (int index = arr.length - 1; index - 1 >= 0; index--) {
                if (arr[index - 1] > arr[index]) {
                    swap(arr, index - 1, index);
                }
            }
        }
    }

复杂度和稳定性

最好时间复杂度 O(N),数据有序时

最坏时间复杂度 O(N^2),数据逆序时

平均时间复杂度 O(N^2)

空间复杂度 O(1)

稳定的

快速排序

思路

1. 从待排序区间选择一个数,作为基准值(pivot);

2. Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;

3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度 == 0,代表没有数据。

代码

下面是递归写法

    public static void quickSort(int[] arr) {
        // 创建一个辅助递归的方法.
        // 在这个方法的参数中, 明确指定针对哪个区间进行递归.
        // [0, length - 1]
        _quickSort(arr, 0, arr.length - 1);
    }

    public static void _quickSort(int[] arr, int left, int right) {
        if (left >= right) {
            // 如果当前的区间为空, 或者只有一个元素
            // 都不需要进行任何处理
            return;
        }
        // 现针对当前 [left, right] 区间进行 partition 操作
        // 方法的返回值, 表示整理完当前区间后, 基准值所在的位置.
        // 遍历过程中的 left 和 right 的重合位置
        int index = partition(arr, left, right);
        // 递归的对左侧区间进行快速排序
        _quickSort(arr, left, index - 1);
        // 递归的对右侧区间进行快速排序
        _quickSort(arr, index + 1, right);
    }

    public static int partition(int[] arr, int left, int right) {
        // 选取最右侧元素作为基准值.
        int v = arr[right];
        int l = left;
        int r = right;
        // 如果 l 和 r 重合, 说明遍历完成
        while (l < r) {
            // 先从左往右, 找一个比基准值大的数字.
            while (l < r && arr[l] <= v) {
                l++;
            }
            // 当循环结束的时候, l 就指向了比基准值大的元素
            // 再从右往左, 找一个比基准值小的数字
            while (l < r && arr[r] >= v) {
                r--;
            }
            swap(arr, l, r);
        }
        // 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
        swap(arr, l, right);
        // 最终方法返回基准值所在的位置(l 和 r 重合的位置)
        return l;
    }

非递归写法

    public static void quickSortByLoop(int[] arr) {
        // 1. 先创建一个栈, 这个栈用来保存当前的每一个待处理区间
        Stack<Integer> stack = new Stack<>();
        // 2. 把根节点入栈, 整个数组对应的区间
        stack.push(0);
        stack.push(arr.length - 1);
        // 3. 循环取栈顶元素
        while (!stack.isEmpty()) {
            // 取的元素就是当前的待处理区间
            // 取的顺序正好和插入的顺序相反
            int right = stack.pop();
            int left = stack.pop();
            if (left >= right) {
                // 如果是空区间或者只有一个元素, 不需要排序
                continue;
            }
            // 调用 partition 方法整理当前区间
            int index = partition(arr, left, right);
            // 右侧区间: [index + 1, right]
            stack.push(index + 1);
            stack.push(right);
            // 左侧区间: [left, index - 1]
            stack.push(left);
            stack.push(index - 1);
        }
    }

    public static int partition(int[] arr, int left, int right) {
        // 选取最右侧元素作为基准值.
        int v = arr[right];
        int l = left;
        int r = right;
        // 如果 l 和 r 重合, 说明遍历完成
        while (l < r) {
            // 先从左往右, 找一个比基准值大的数字.
            while (l < r && arr[l] <= v) {
                l++;
            }
            // 当循环结束的时候, l 就指向了比基准值大的元素
            // 再从右往左, 找一个比基准值小的数字
            while (l < r && arr[r] >= v) {
                r--;
            }
            swap(arr, l, r);
        }
        // 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
        swap(arr, l, right);
        // 最终方法返回基准值所在的位置(l 和 r 重合的位置)
        return l;
    }

复杂度和稳定性

最好时间复杂度 O(NlogN),当每次选择基准值都为区间中位数时,达到了折半的目的

最坏时间复杂度 O(N^2),当每次选择区间都为数据的最大值或者最小值时

平均时间复杂度 O(NlogN)

空间复杂度主要由递归提供

最好空间复杂度 O(logN),当每次选择基准值都为区间中位数时,达到了折半的目的

最坏空间复杂度 O(N),当每次选择区间都为数据的最大值或者最小值时

平均空间复杂度 O(logN)

不稳定的。

归并排序

思路

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

代码

递归写法

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

    // 辅助递归的方法
    public static void _mergeSort(int[] arr, int left, int right) {
        if (right - left <= 1) {
            // 判定当前区间是不是只有一个元素或者没有元素
            // 此时不需要进行排序
            return;
        }
        int mid = (left + right) / 2;
        // 先让 [left, mid) 区间变成有序
        _mergeSort(arr, left, mid);
        // 再让 [mid, right) 区间变成有序
        _mergeSort(arr, mid, right);
        // 合并两个有序区间
        merge(arr, left, mid, right);
    }

    // 归并排序中的关键操作, 就是归并两个有序数组.
    // 使用该 merge 方法完成数组归并的过程
    // 此处两个数组就通过参数的 left, mid, right 描述
    // [left, mid) 左侧数组
    // [mid, right) 右侧数组
    public static void merge(int[] arr, int left, int mid, int right) {
        // 进行具体的归并操作
        // 需要创建一个临时的空间用来保存归并的结果
        // 临时空间得能保存下带归并的两个数组.
        // right - left 这么长
        if (left >= right) {
            // 空区间就直接忽略~~
            return;
        }
        int[] tmp = new int[right - left];
        int tmpIndex = 0; // 这个下标表示当前元素该放到临时空间的哪个位置上.
        int cur1 = left;
        int cur2 = mid;
        while (cur1 < mid && cur2 < right) {
            // 此处 最好写成 <= , 目的就是稳定性.
            // 由于 cur1 是在左侧区间, cur2 是在右侧区间.
            // 此时如果发现 cur1 和 cur2 的值相等,
            // 就希望左侧区间的 cur1 在最终结果中仍然是在左侧.
            // 于是就先把 cur1 对应的元素给先放到结果中.
            if (arr[cur1] <= arr[cur2]) {
                // 把 cur1 对应的元素插入到临时空间中
                tmp[tmpIndex] = arr[cur1];
                tmpIndex++;
                cur1++;
            } else {
                // 把 cur2 对应的元素插入到临时空间中
                tmp[tmpIndex] = arr[cur2];
                tmpIndex++;
                cur2++;
            }
        }
        // 循环结束之后, 需要把剩余的元素也都给拷贝到最终结果里.
        while (cur1 < mid) {
            tmp[tmpIndex] = arr[cur1];
            tmpIndex++;
            cur1++;
        }
        while (cur2 < right) {
            tmp[tmpIndex] = arr[cur2];
            tmpIndex++;
            cur2++;
        }
        // 还需要把 tmp 的结果再放回 arr 数组. (原地排序)
        // 把原始数组的 [left, right) 区间替换回排序后的结果
        for (int i = 0; i < tmp.length; i++) {
            arr[left + i] = tmp[i];
        }
    }

非递归写法

    public static void mergeSortByLoop(int[] arr) {
        // gap 用于限定分组.
        // gap 值的含义就是每个待归并数组的长度
        int gap = 1;
        for (; gap < arr.length; gap *= 2) {
            // 当前两个待归并的数组
            for (int i = 0; i < arr.length; i += 2*gap) {
                // 在这个循环中控制两个相邻的数组进行归并
                // [left, mid) 和 [mid, right) 就要进行归并
                int left = i;
                int mid = i + gap;
                if (mid >= arr.length) {
                    mid = arr.length;
                }
                int right = i + 2 * gap;
                if (right >= arr.length) {
                    right = arr.length;
                }
                merge(arr, left, mid, right);
            }
        }
    }

复杂度和稳定性

时间复杂度 O(NlogN)

空间复杂度 O(N)

稳定的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值