排序

我们平时常用的排序有四大类,分别为: 插入排序(直接插入排序,希尔排序),选择排序(选择排序,堆排序),交换排序(冒泡排序,快速排序),和归并排序.

下面我们来逐一分析实现它们

1.直接插入排序

直接插入排序在保证前面的元素已经有序时,一个一个的加入,向后平移的过程.
其次,它是稳定的,时间复杂度为最坏为O(n^2), 最好为O(n), 平均为O(n^2),空间复杂度为O(1) .
插入排序 : 数组越接近有序,速度越快 数组规模足够小时,插排最优快.(好)

public static void insertSort(int[] array) {
    // bound 变量来把整个数组分成两个区间
    // [0, bound) 已排序区间
    // [bound, size) 待排序区间
    for (int bound = 1; bound < array.length; bound++) {
        // bound 下标对应的元素就是待插入元素.
        // 把这个元素放到前面的有序顺序表中的合适位置
        int tmp = array[bound];
        int cur = bound - 1;
        for (; cur >= 0; cur--) {
            if (array[cur] > tmp) {
                array[cur + 1] = array[cur];
            } else {
                break;
            }
        }
        array[cur + 1] = tmp;
    }
}
2.希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时, 所有记录在统一组内排好序。

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。

时间复杂度为最坏为O(n^2), 最好为O(n), 平均为O(n^1.3),空间复杂度为O(1),不稳定

public static void shellSort(int[] array) {
    int gap = array.length;
    while (gap > 1) {
        insertSortGap(array, gap);
        gap = gap / 2;
    }
    insertSortGap(array, 1);
}

private static void insertSortGap(int[] array, int gap) {
    for (int bound = 1; bound < array.length; bound++) {
        int tmp = array[bound];
        int cur = bound - gap;
        // 同组之内的相邻元素之间下标差了 gap
        for (; cur >= 0; cur -= gap) {
            if (array[cur] > tmp) {
                array[cur + gap] = array[cur];
            } else {
                break;
            }
        }
        array[cur + gap] = tmp;
    }
}
3.选择排序

简单选择排序是选择排序中的一种,每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完
。 它是不稳定的. 它的时间复杂度最好和最坏都为N^2,空间复杂度为O(1).

public static void selectSort(int[] array) {
    // [0, bound) 已排序区间
    // [bound, size) 待排序区间
    for (int bound = 0; bound < array.length; bound++) {
        for (int cur = bound + 1; cur < array.length; cur++) {
            if (array[cur] < array[bound]) {
                swap(array, cur, bound);
            }
        }
    }
}
4.堆排序

堆排序是另外一种选择排序,堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是
通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。 它不稳定. 时间复杂度最好最坏都是O(n * log(n))

public static void heapSort(int[] array) {
    // 1. 创建堆
    createHeap(array);
    // 2. 循环取出堆顶的最大值, 放到最后面
    for (int i = 0; i < array.length; i++) {
        // 待排序区间: [0, array.length - i)
        // 已排序区间: [array.length - i, array.length)
        swap(array, 0, array.length - i - 1);
        // 第一个参数是数组
        // 第二个参数是数组中的有效元素的个数
        // 第三个参数是从哪个位置进行向下调整
        shiftDown(array, array.length - i, 0);
    }
}

private static void createHeap(int[] array) {
    // 从最后一个非叶子节点, 开始出发, 从后往前向下调整
    for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
        shiftDown(array, array.length, i);
    }
}

// O(logN)
private static void shiftDown(int[] array, int size, int index) {
    int parent = index;
    int child = 2 * parent + 1;
    while (child < size) {
        if (child + 1 < size
            && array[child + 1] > array[child]) {
            child = child + 1;
        }
        // 经历了上面的 if 之后, child 指向左右子树的最大值
        if (array[child] > array[parent]) {
            // 建立大堆.
            swap(array, child, parent);
        } else {
            break;
        }
        parent = child;
        child = 2 * parent + 1;
    }
}
5.冒泡排序

冒泡排序是在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序
它是稳定的
时间复杂度为最坏为O(n^2), 最好为O(n), 平均为O(n^2),空间复杂度为O(1),

public static void bubbleSort(int[] array) {
    // [0, bound) 已排序区间
    // [bound, size) 待排序区间
    for (int bound = 0; bound < array.length; bound++) {
        for (int cur = array.length - 1; cur > bound; cur--) {
            if (array[cur - 1] > array[cur]) {
                swap(array, cur - 1, cur);
            }
        }
    }
}
6.快速排序

快速排序基本原理为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
时间复杂度最好为O(n * log(n)),最坏为 O(n^2) ,平均为O(n * log(n))
它是不稳定的
空间复杂度最好为O(log(n)) ,最坏为O(n),平均为O(log(n)) .

public static void quickSort(int[] array) {
    // 参数的含义表示针对数组中的那段区间进行快速排序
    // [0, array.length - 1]
    quickSortHelper(array, 0, array.length - 1);
}

private static void quickSortHelper(int[] array, int left, int right) {
    if (left >= right) {
        // 如果只有一个元素或者没有元素, 都不需要排序
        return;
    }
    // 这个方法就是刚才进行区间整理的方法
    // 选取基准值, 并且把小于基准值的放到左侧, 大于基准值的放到右侧
    // 返回值 [left, right] 最终整理完毕后, 基准值的下标
    int index = partition(array, left, right);
    quickSortHelper(array, left, index - 1);
    quickSortHelper(array, index + 1, right);
}

private static int partition(int[] array, int left, int right) {
    // 基准值
    int baseIndex = left;
    int baseValue = array[baseIndex];
    while (left < right) {
        while (left < right && array[right] >= baseValue) {
            right--;
        }
        // 循环结束之后, right 指向的位置, 就是从右往左第一个比基准值小的元素
        while (left < right && array[left] <= baseValue) {
            left++;
        }
        // 循环结束之后, left 指向的位置, 就是从左往右第一个比基准值大的元素.

        // 交换 left 和 right 位置的元素
        swap(array, left, right);
    }
    // 当前这个代码是把 left 和 right 重合位置的元素和基准值交换.
    // 基准值选取的是最后一个元素, 如果要想交换
    // 前提是 left 和 right 重合位置对应的元素, 必须得比基准值大.
    // 循环结束有两种情况:
    // 1. left++导致的循环结束
    //    上次循环过程中, 进行了一个 swap 操作. 经过这个 swap 操作之后,
    //    right 一定指向一个大于基准值的元素. 此时如果 left 和 right 重合
    //    也一定是指向一个大于基准值的元素
    // 2. right-- 导致的循环结束
    //    此时由于 left 刚刚找到一个比基准值大的元素, 此时 right 和 left
    //    重合之后, 对应的元素也就是刚才的那个比基准值大的值
    swap(array, left, baseIndex);
    return left;
}

非递归版本的快速排序

public static void quickSortByLoop(int[] array) {
    // 1. 先创建一个栈, 栈里面存的是待处理区间的下标
    Stack<Integer> stack = new Stack<>();
    // 2. 初始情况下待处理区间, 就是整个数组
    stack.push(array.length - 1);
    stack.push(0);
    while (!stack.isEmpty()) {
        // 3. 取栈顶元素, 栈顶元素就是我们要处理的区间
        int left = stack.pop();
        int right = stack.pop();
        if (left >= right) {
            continue;
        }
        // 4. 对当前待处理区间进行整理
        int index = partition(array, left, right);
        // 5. 接下来要处理的区间再入栈
        // [left, index - 1]
        // [index + 1, right]
        stack.push(index - 1);
        stack.push(left);

        stack.push(right);
        stack.push(index + 1);
    }
}
7.归并排序

归并排序是一种分治思想,类似于二叉树的后序.归并排序借助临时空间. 归并排序实质是一个后序排列.
时间复杂度O(n * log(n))
空间复杂度O(n)
它是稳定的.

public static void mergeSort(int[] array) {
    // 后两个参数表示要进行归并排序的区间.
    // [0, array.length)
    // new 足够大的数组, 把这个数组作为缓冲区传给
    // 递归函数
    mergeSortHelper(array, 0, array.length);
}

private static void mergeSortHelper(int[] array, int left, int right) {
    // [left, right) 构成了要去进行归并排序的区间
    // 如果区间为空区间, 或者只有一个元素, 都不用排序
    if (left >= right || right -  left == 1) {
        // 空区间或者区间只有一个元素, 都不需要进行归并排序
        return;
    }
    // 使用类似后序遍历的方式.
    // 先把当前的待排序区间拆成两半,
    // 递归的对这两个子区间进行归并排序, 保证两个区间有序之后
    // 再进行合并
    int mid = (left + right) / 2;
    // [left, mid)
    // [mid, right)
    mergeSortHelper(array, left, mid);
    mergeSortHelper(array, mid, right);
    merge(array, left, mid, right);
}

private static void merge(int[] array, int left,
                          int mid, int right) {
    // 创建一段临时空间辅助进行归并
    // 这个临时空间的长度应该是两个待归并区间的长度之和
    int length = right - left;
    int[] output = new int[length];
    // 这个变量保存着当前 output 中的末尾元素的下标
    int outputIndex = 0;
    // i 和 j 是用来遍历两个区间的辅助变量
    // [left, mid)
    // [mid, right)
    int i = left;
    int j = mid;
    while (i < mid && j < right) {
        // 此处的 if 条件必须要 <= , 否则没法保证稳定性
        if (array[i] <= array[j]) {
            // i 对应的元素比 j 小
            // 就把 i 对应的元素插入到 output 末尾
            output[outputIndex++] = array[i++];
        } else {
            output[outputIndex++] = array[j++];
        }
    }
    // 上面的循环结束之后, 两个区间至少有一个是遍历完了的.
    // 就把剩下的区间的内容直接拷贝到 output 中即可.
    while (i < mid) {
        output[outputIndex++] = array[i++];
    }
    while (j < right) {
        output[outputIndex++] = array[j++];
    }

    // 最后一步, 把 output 中的元素拷贝回原来的区间
    for (int k = 0; k < length; k++) {
        array[left + k] = output[k];
    }
}

public static void mergeSortByLoop(int[] array) {
    // 借助下标相关的规律来进行分组.
    // 初始情况下, 每个元素单独作为一组
    // [0] [1]    [2] [3]     [4] [5]
    // [0, 1] 和 [2, 3] 合并. [4, 5]  和 [6, 7] 区间合并
    // [0, 1, 2, 3]  [4, 5, 6, 7]
    for (int gap = 1; gap < array.length; gap *= 2) {
        for (int i = 0; i < array.length; i += 2 * gap) {
            // 这个循环负责在 gap 为指定值的情况下
            // 把所有的区间进行归并
            // 针对当前的 i, 也能划分出两个需要进行归并的区间
            // [beg, mid)
            // [mid, end)
            int beg = i;
            int mid = i + gap;
            int end = i + 2 * gap;
            if (mid > array.length) {
                mid = array.length;
            }
            if (end > array.length) {
                end = array.length;
            }
            merge(array, beg, mid, end);
        }
    }
}

private static void swap(int[] array, int x, int y) {
    int tmp = array[x];
    array[x] = array[y];
    array[y] = tmp;
}
8.七种排序算法的性能比较
排序方法最好平均最坏空间复杂度稳定性
冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
插入排序O(n)O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
希尔排序O(n)O(n^1.3)O(n^2)O(1)不稳定
堆排序O(n * log(n))O(n * log(n))O(n * log(n))O(1)不稳定
快速排序O(n * log(n))O(n * log(n))O(n^2)O(log(n)) ~ O(n)不稳定
归并排序O(n * log(n))O(n * log(n))O(n * log(n))O(n)稳定
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值