排序【数据结构】

排序

    • 1.插入排序:
      • 直接插入排序
      • 希尔排序
    • 2.选择排序
      • 选择排序
      • 堆排序
    • 3.交换排序
      • 冒泡排序
      • 快速排序
        • 递归实现快排
        • 非递归实现快排
    • 4.归并排序
      • 归并排序
        • 递归实现归并排序
        • 非递归实现归并排序:
    • 睡眠排序
    • 总结

概念:
排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作

排序规则:
一般是升序降序排列,若待排序元素比较复杂,就会有更复杂的排序方式

常见排序算法:

排序【数据结构】_第1张图片

1.插入排序:

直接插入排序

插入排序基于顺序表的插入来实现的

直接插入排序的基本操作是将一个记录插入到已排好序的有序表里,从而得到一个新的、记录数+1的有序表

举例:

排序【数据结构】_第2张图片
代码示例:

public static void insertSort(int[] array){
     
    //通过bound来划分出两个区间
    // [0,bound) 已排序区间
    // [bound,size) 待排序区间
    for (int bound = 1; bound < array.length; bound++) {
     
        int v = array[bound];
        // cur 表示已排序区间的最有一个元素下标
        int cur = bound - 1;
        for (; cur >= 0; cur--) {
     
            if(array[cur] > v){
     
                array[cur + 1] = array[cur];
            }
            // 不需要搬运
            else {
     
                break;
            }
        }
        array[cur + 1] = v;
    }
}

直接插入排序特点:

  • 当待排序区间元素比较少的时候,排序效率很高
  • 当整个数组比较接近有序的时候,排序效率也很高

直接插入排序性能分析:

时间复杂度 —— O(N2)
空间复杂度 —— O(1)
稳定性: 稳定排序

希尔排序

进阶版本的插入排序
先分组,针对每个组进行(直接)插入排序,逐渐缩小组的个数,最终整个数组就有序

画图分析:

常见 gap 取值:size,size / 2,size / 4…1

排序【数据结构】_第3张图片

代码实现:

//希尔排序
public static void shellSort(int[] array){
     
    int gap = array.length / 2;
    while (gap > 1){
     
        // 需要循环进行分组插排
        insertSortGap(array,gap);
        gap = gap / 2;
    }
    insertSortGap(array,1);
}
private static void insertSortGap(int[] array, int gap) {
     
    //当把 gap 换成1时,理论上该排序与插排一模一样
    for (int bound = gap; bound < array.length; bound++) {
     
        int v = array[bound];
        //找同组中的上一个元素
        int cur = bound - gap;
        // 每次找同组中的相邻元素,同组元素中的下标差值就是gap
        for (; cur >= 0; cur -= gap) {
     
            //此处若带 =,那么插入排序就不稳定了
            if(array[cur] > v){
     
                array[cur + gap] = array[cur];
            }
            // 不需要搬运,此时说明找到了合适的位置
            else {
     
                break;
            }
        }
        array[cur + gap] = v;
    }
}

希尔排序性能分析:

时间复杂度 —— 理论极限:O(N1.3) 若按照 size / 2,size / 4…这种方式设置gap,则为: O(N2)
空间复杂度 —— O(1)
稳定性: 不稳定

2.选择排序

选择排序

基于打擂台的的思想,每次从数组中找出最小值,然后把最小值放在合适的位置

排序【数据结构】_第4张图片
代码实现:

//选择排序
public static void selectSort(int[] array){
     
    //[0,bound) 已排序区间
    //[bound,size) 待排序区间
    for (int bound = 0; bound < array.length; bound++) {
     
        //以bound位置元素为 擂主,循环从待排序区间取出元素和擂主进行比较
        for (int cur = bound + 1; cur < array.length; cur++) {
     
            //若 打擂 成功,就和擂主交换位置
            if(array[cur] < array[bound]){
     
                int tmp = array[cur];
                array[cur] = array[bound];
                array[bound] = tmp;
            }
        }
    }
}

选择排序性能分析:

时间复杂度 —— O(N2)
空间复杂度 —— O(1)
稳定性: 不稳定
选择排序思考非常好理解,但是效率不太好,实际中很少使用

堆排序

升序排序:

  1. 把数组建立一个小堆,取出最小值放到另外一个数组中,循环取堆顶元素,尾插到新数组中(升级版本的选择排序 哦吼吼~~) (小缺陷: 新数组,需要额外 O(N) 的空间)
  2. 把数组建立一个大堆,把堆顶元素和堆的最后一个元素互换,把最后一个元素删除,再从堆顶向下调整(空间复杂度:O(1) )

排序【数据结构】_第5张图片

代码实现:

//堆排序
public static void heapSort(int[] array){
     
    //先建堆
    createHeap(array);
    //循环把堆顶元素交换到最后,同时向下调整堆
    //当堆中只剩最后一个元素时,已有序,不需要再调整  循环次数:length-1
    for (int i = 0; i < array.length - 1; i++) {
     
        //交换堆顶元素和堆的最后一个元素
        //堆的个数,相当于 array.length - i
        //堆的最后一个元素下标为:array.length - i - 1
        swap(array,0,array.length - i - 1);
        //交换完成后,堆再次缩水  array.length - i - 1
        //从堆中删除最后一个元素
        //数组中
        // [0,array.length-i-1) 待排序区间
        // [array.length - i - 1,array.length) 已排序区间

        //向下调整
        shiftDown(array,array.length - i - 1,0);

    }
}
private static void createHeap(int[] array) {
     
    //从最后一个非叶子节点向前循环,向下调整
    for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
     
        shiftDown(array,array.length,i);
    }
}
private static void shiftDown(int[] array, int heapLength, int index) {
     
    //升序排序 建大堆
    int parent = index;
    int child = 2 * parent + 1;
    while (child < heapLength){
     
        if(child + 1 < heapLength && array[child + 1] > array[child]){
     
            child = child + 1;
        }
        //child 是左右子树较大值的下标
        if(array[child] > array[parent]){
     
            swap(array,child,parent);
        }
        else{
     
            break;
        }
        parent = child;
        child = 2 * parent + 1;
    }
}
private static void swap(int[] array,int i,int k){
     
    int tmp = array[i];
    array[i] = array[k];
    array[k] = tmp;
}

堆排序的性能分析:

时间复杂度 —— O(N * logN)
空间复杂度 —— O(1)
稳定性: 不稳定

3.交换排序

冒泡排序

核心目标和堆排序、选择排序都很像,每次找到一个最大值 / 最小值,并放到一个合适的位置
借助相邻元素比较交换方式来找
排序【数据结构】_第6张图片
代码实现:

//冒泡排序
public static void bubbleSort(int[] array){
     
    for (int bound = 0; bound < array.length; bound++) {
     
        // [0,bound) 待排序区间
        // [bound,size) 已排序区间
        for (int cur = 0; cur < bound; cur++) {
     
            // 后一个 小于 前一个,就交换
            if(array[cur] > array[cur + 1]){
     
                swap(array,cur,cur + 1);
            }
        }
    }
}

冒泡排序性能分析:

时间复杂度 —— O(N2)
空间复杂度 —— O(1)
稳定性: 稳定排序

快速排序

递归实现快排

依赖递归

  • 首先在待排序区间中,找到一个基准值(常见可以取区间的第一个 / 最后一个元素)
  • 以基准值为中心,把整个区间整理成三部分
    左侧部分元素 ≤ 基准值
    右侧部分元素 ≥ 基准值
  • 再次针对左侧 / 右侧整理好的区间,进一步进行递归,重复上述整理过程

注意:若取最右侧值为基准值的话,必须先从右往左找,后从左往右找
若取最左侧值为基准值的话,必须先从左往右找,后从右往左找

画图分析:

排序【数据结构】_第7张图片

// 快速排序
public static void quickSort(int[] array){
     
    //辅助完成递归过程
    quickSortHelper(array,0,array.length - 1);
}
private static void quickSortHelper(int[] array, int left, int right) {
     
    if(left >= right){
     
        //区间中有 0个 或 1个 元素,不需要排序
        return;
    }
    //针对 [left,right] 区间进行整理
    // index 表示整理完成后,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 begin = left;
    int end = right;
    //取最右侧元素为基准值
    int base = array[right];
    while (begin < end){
     
        //从左往右找 > 基准值的
        while (begin < end && array[begin] <= base){
     
            begin++;
        }
        //循环结束后, begin 要么和 end 重合,要么指向一个 > base的值
        //从右往左找 < 基准值的
        while (begin < end && array[end] >= base){
     
            end--;
        }
        //循环结束后, end 要么和 begin 重合,要么指向一个 < base的值
        //交换 begin end 的值
        swap(array,begin,end);
    }
    // 当 i k重合时,把重合位置的元素 与 基准值交换
    swap(array,begin,right);
    return begin;
}

快速排序性能分析:快速排序的效率与基准值取的好坏密切相关
若基准值是一个接近数组中位数的的元素,则划分出的左右区间就比较均衡,此时效率就比较高
若基准值是数组的最大 / 小值,则划分出的左右区间就不均衡,此时效率就低

若数组正好是反序,此时快排就变成了 "慢排"
最坏 时间复杂度: O(N2)

平均时间复杂度 —— O(N * logN)
平均空间复杂度 —— O(logN)
最坏空间复杂度 —— O(N)
稳定性: 不稳定排序

非递归实现快排

借助栈来模拟递归过程

代码实现:

//非递归实现快排
public static void quickSortByLoop(int[] array){
     
    //借助栈,模拟实现递归的过程
    //stack用来存放数组下标
    Stack<Integer> stack = new Stack<>();
    //初始情况下,先把右侧边界下标入栈,再把左侧边界下标入栈
    // 左右边界 [ , ]
    stack.push(array.length - 1);
    stack.push(0);
    while (!stack.isEmpty()){
     
        //取出栈顶元素,取元素的顺序要和push的顺序相反
        int left = stack.pop();
        int right = stack.pop();
        //只有 1个 或0 个元素,不需要整理
        if(left >= right){
     
            continue;
        }
        //通过 partition ,把区间整理成,左侧 ≤ 基准值,右侧 ≥ 基准值
        int index = partition(array,left,right);
        //准备处理下个区间
        // [index+1,right) 基准值右侧区间
        stack.push(right);
        stack.push(index + 1);

        // [left,index-1) 基准值右侧区间
        stack.push(index - 1);
        stack.push(left);
    }
}

快速排序的优化:
.
1.优化基准值的取法 — 三个位置取中
最左侧元素,中间位置元素,最右侧元素,取中间值作为基准值,把确认的基准值交换到数组末尾或者开始位置
2.区间较小,直接插入排序
区间较小时,再去进行递归的话,效率较低,直接进行插入排序即可
3.区间特别大,使用堆排序
若区间特别大,递归的深度也会非常深,当递归深度到达一定程度时,把当前区间的排序使用堆排序来进行优化

4.归并排序

归并排序

归并排序有两个重要特点,可以适用于外部排序,也可以适用于链表排序

前边的几种排序,都是基于数字存在内存中,并且数据存在数组中,数据在内存中的,就叫内部排序
数据存在磁盘中,那就是外部排序

希尔排序,堆排序,快速排序,都依赖于随机访问能力,不太适合针对链表排序

递归实现归并排序

基本思路:
思路来源于经典问题,把两个有序链表 / 数组合并成一个
归并的前提是:两个待归并区间都是有序的

画图分析:

排序【数据结构】_第8张图片
代码实现:

// 归并排序
public static void mergeSort(int[] array){
     
    mergeSortHelper(array,0,array.length);
}
private static void mergeSortHelper(int[] array, int low, int high) {
     
    //[low,high)
    //区间只有 0个 或 1个 元素
    if(high - low <= 1){
     
        return;
    }
    int mid = (low + high) / 2;
    mergeSortHelper(array,low,mid);
    // 方法执行完, [low,mid) 排序完成
    mergeSortHelper(array,mid,high);
    // 方法执行完, [mid,high) 排序完成
    // 两个区间已有序 针对两个有序区间进行合并
    merge(array,low,mid,high);
}
// [low,mid) 有序区间
// [mid,high) 有序区间
// 把上述两个有序区间合并成一个有序区间
public static void merge(int[] array,int low,int mid,int high){
     
    int[] output = new int[high - low];
    // 记录当前output数组中放入多少个元素
    int outputIndex = 0;
    int cur1 = low;
    int cur2 = mid;
    while (cur1 < mid && cur2 < high){
     
        if(array[cur1] <= array[cur2]){
     
            output[outputIndex] = array[cur1];
            outputIndex++;
            cur1++;
        }
        else{
     
            output[outputIndex] = array[cur2];
            outputIndex++;
            cur2++;
        }
    }
    //循环结束后,cur1 或 cur2 到达末尾,剩下的一个还有内容
    // 把剩下的内容全部拷贝到output中
    while (cur1 < mid){
     
        output[outputIndex] = array[cur1];
        outputIndex++;
        cur1++;
    }
    while (cur2 < high){
     
        output[outputIndex] = array[cur2];
        outputIndex++;
        cur2++;
    }
    //把output中的元素拷贝到原来的数组
    for (int i = 0; i < high - low; i++) {
     
        array[low + i] = output[i];
    }
}

归并排序性能分析:

时间复杂度 —— O(N * logN)
空间复杂度 —— O(N)
若针对链表归并,空间复杂度可以是:O(1)
稳定性: 稳定排序

非递归实现归并排序:

代码实现:

public static void mergeSortByLoop(int[] array){
     
    // 利用gap 变量进行分组
    // 当 gap 为1时,[0] [1]进行合并,[2] [3]进行合并...
    // 当 gap 为2时,[0,1]和[2,3]进行合并..[4,5]和[6,7]进行合并..
    // 当 gap 为4时,[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){
     
            //循环一次,就完成两个相邻组的合并
            //相邻组
            //[begin,mid)  begin -> i
            //[mid,end)    mid -> i+gap
            // end -> i + 2*gap
            int begin = 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,begin,mid,end);
        }
    }
}

睡眠排序

时间复杂度为:O(0),几乎不吃CPU

举例:9 5 2 7 3 6 8
取到9:创建线程,sleep(9),再打印 9
取到5:创建线程,sleep(5),再打印 5
.
.
.
sleep操作:让代码放弃CPU,偷懒睡觉~

总结


排序【数据结构】_第9张图片
排序【数据结构】_第10张图片

你可能感兴趣的:(数据结构和算法,Java,数据结构,排序算法,算法,Java)