当前位置: 首页 > news >正文

[数据结构]排序 --2

目录

8、快速排序

8.1、Hoare版

8.2、挖坑法

8.3、前后指针法

9、快速排序优化

9.1、三数取中法

9.2、采用插入排序

10、快速排序非递归

11、归并排序

12、归并排序非递归

13、排序类算法总结

14、计数排序

15、其他排序

15.1、基数排序

15.2、桶排序


8、快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法

基本思想:任取待排序元素序列中的某元 素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

8.1、Hoare版

1. 把第一个值作为基准值 pivot

2. right 从右边走,遇到比 pivot 大的就停下;left 从左边走,遇到比 pivot 小的就停下

3. 交换 left 和 right 的值

4. left 和 right 继续走,直到 left 和 right 相遇

5. 相遇的位置就是要找的位置,把基准值与该位置交换

    public static void quickSort(int[] array) {
        quick(array,0,array.length-1);
    }
    private static void quick(int[] array,int start,int end) {
        if(start >= end) {
            return;
        }
        int pivot = partitionHoare(array,start,end);
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }
    private static int partitionHoare(int[] array, int left, int right) {
        int tmp = array[left];
        int pivot = left;
        while (left < right) {
            // 单独的循环 不能一直减到超过最左边的边界
            while (left < right && array[right] >= tmp) {
                right--;
            }
            while (left < right && array[left] <= tmp) {
                left++;
            }
            swap(array,left,right);
        }
        swap(array,pivot,left);
        return left;
    }

两个问题:

1. 为什么 array[right] >= tmp 必须带等于号

        可能会出现 left 和 right 无限交换的死循环

2. 为什么从 right 先走而不是 left

        如果 left 先走可能会出现相遇的是比基准大的数据,最后把大的数据放到了最前面

快速排序总结:

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

2. 时间复杂度:O(N*logN)

        最好的情况下:O(N*logN)

        最坏情况下:O(N^2)  -- 逆序/有序

3. 空间复杂度:O(logN)  -- 递归了logN层

        最好的情况下:O(logN)

        最坏情况下:O(N)   -- 逆序/有序

4. 稳定性:不稳定

8.2、挖坑法

1. 把第一个值拿出来作为基准值 tmp,第一个值的位置就是第一个坑

2. right 从右边走,遇到比 pivot 大的就停下,然后把这个值放到上一个坑里,right 就形成了新的坑

3. left 从左边走,遇到比 tmp 小的就停下,然后把这个值放到坑里,left 就是新的坑

4. 循环2、3,直到 left 和 right 相遇

5. 相遇的位置就是要找的位置,把基准值放在这个位置

    public static void quickSort(int[] array) {
        quick(array,0,array.length-1);
    }
    private static void quick(int[] array,int start,int end) {
        if(start >= end) {
            return;
        }
        int pivot = partitionHole(array,start,end);
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }
    private static int partitionHole(int[] array, int left, int right) {
        int tmp = array[left];
        while (left < right) {
            // 单独的循环 不能一直减到超过最左边的边界
            while (left < right && array[right] >= tmp) {
                right--;
            }
            array[left] = array[right];

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

8.3、前后指针法

1. 定义两根指针cur和prev,初始位置如下图

2. cur开始往后走,如果遇到比key小的值,则++prev,然后交换prev和cur指向的元素,再++cur,如果遇到比key大的值,则只++cur

3. 当cur访问过最后一个元素后,将key的元素与prve访问的元素交换位置。cur访问完整个数组后的各元素位置如下图

private static int partition(int[] array, int left, int right) {
    int prev = left ;
    int cur = left+1;
    while (cur <= right) {
        if(array[cur] < array[left] && array[++prev] != array[cur]) {
            swap(array,cur,prev);
        }
        cur++;
    }
    swap(array,prev,left);
    return prev;
}

总结:
1. Hoare
2. 挖坑法
3. 前后指针法
这3种方式  每次划分之后的前后顺序 有可能是不一样的

9、快速排序优化

优化的出发点:减少递归的次数

9.1、三数取中法

既然有序数组或者有序数组片段会使效率下降,我们就可以让基准值每次都取大小靠中的数,然后在进行快速排序这样就可以避免了。但不是完全避免,只是减少了最坏情况出现的概率,最坏情况还是O(n²),但有效提升了运行效率,主要提升的部分是数组中有序的数组片段,减少了循环次数。

    // 求中位数的下标
    private static int middleNum(int[] array,int left,int right) {
        int mid = (left+right)/2;
        if(array[left] < array[right]) {
            if(array[mid] < array[left]) {
                return left;
            }else if(array[mid] > array[right]) {
                return right;
            }else {
                return mid;
            }
        }else {
            //array[left] > array[right]
            if(array[mid] < array[right]) {
                return right;
            }else if(array[mid] > array[left]) {
                return left;
            }else {
                return mid;
            }
        }
    }
    private static void quick(int[] array,int start,int end) {
        if(start >= end) {
            return;
        }

        //1 2 3 4 5 6 7
        int index = middleNum(array,start,end);
        swap(array,index,start);
        //4 2 3 1 5 6 7

        int pivot = partition(array,start,end);
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }

9.2、采用插入排序

往往一棵树的最后两层的结点占整棵树的绝大多数,所以当递归到一定深度时,采用直接插入排序

    public static void insertSort(int[] array,int left,int right) {
        for (int i = left+1; i <= right; i++) {
            int tmp = array[i];
            int j = i-1;
            for (; j >= left ; j--) {
                if(array[j] > tmp) {
                    array[j+1] = array[j];
                }else {
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }
    private static void quick(int[] array,int start,int end) {
        if(start >= end) {
            return;
        }
        if(end - start + 1 <= 15) {
            insertSort(array, start, end);
            return;
        }

        //1 2 3 4 5 6 7
        int index = middleNum(array,start,end);
        swap(array,index,start);
        //4 2 3 1 5 6 7

        int pivot = partition(array,start,end);
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }

10、快速排序非递归

1. 先调用partition方法找到基准
2. 基准左边和右边有没有2个及以上个元素,有就把下标放到栈中

3. 判断栈空不空,不空出栈2个,第一个是新的end,第二个是新的start

4. 栈不为空时,循环执行上述1.2.3

    public static void quickSortNor(int[] array) {
        int start = 0;
        int end = array.length-1;
        Stack<Integer> stack = new Stack<>();
        int pivot = partition(array,start,end);
        if(pivot > start+1) {
            stack.push(start);
            stack.push(pivot-1);
        }
        if(pivot+1 < end) {
            stack.push(pivot+1);
            stack.push(end);
        }
        while (!stack.isEmpty()) {
            end = stack.pop();
            start = stack.pop();
            pivot = partition(array,start,end);
            if(pivot > start+1) {
                stack.push(start);
                stack.push(pivot-1);
            }
            if(pivot+1 < end) {
                stack.push(pivot+1);
                stack.push(end);
            }
        }
    }

11、归并排序

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

    public static void mergeSort(int[] array) {
        mergeSortFun(array,0,array.length-1);
    }

    private static void mergeSortFun(int[] array,int start,int end) {
        if(start >= end) {
            return;
        }
        int mid = (start+end)/2;
        mergeSortFun(array,start,mid);
        mergeSortFun(array,mid+1,end);
        //合并
        merge(array,start,mid,end);
    }

    private static void merge(int[] array, int left, int mid, int right) {
        // s1,e1,s2,e2 可以不定义,这样写为了好理解
        int s1 = left;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = right;
        //定义一个新的数组
        int[] tmpArr = new int[right-left+1];
        int k = 0;//tmpArr数组的下标
        //同时满足 证明两个归并段 都有数据
        while (s1 <= e1 && s2 <= e2) {
            if(array[s1] <= array[s2]) {
                tmpArr[k++] = array[s1++];
            }else {
                tmpArr[k++] = array[s2++];
            }
        }
        while (s1 <= e1) {
            tmpArr[k++] = array[s1++];
        }
        while (s2 <= e2) {
            tmpArr[k++] = array[s2++];
        }
        //把排好序的数据 拷贝回原来的数组array当中
        for (int i = 0; i < tmpArr.length; i++) {
            array[i+left] = tmpArr[i];
        }
    }

两个有序数组合并为一个有序数组代码:

public static int[] mergeArray(int[] arrayl,int[] array2) {
    // 注意判断参数
    int[] tmp = new int[array1.length+array2.length];
    int k = 0;
    int s1 = 0;
    int el = array1.length-1;
    int s2 = 0;
    int e2 = array2.length-1;
    while (s1 <= el && s2 <= e2) {
        if(array1[s1] <= array2[s2]) {
            tmp[k++] = array1[s1++];
        }else {
            tmp[k++]=array2[s2++];
        }
    }
    while (s1 <= el) {
        tmp[k++] = array1[s1++];
    }
    while (s2 <= e2) {
        tmp[k++]= array2[s2++];
    }
    return tmp;
}

归并排序总结
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)

        ‌递归调用栈空间:O(logN)

        合并操作空间:O(N)
4. 稳定性:稳定


海量数据的排序

外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1. 先把文件切分成 200 份,每个 512 M
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3. 进行2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了(在外部存储进行)

12、归并排序非递归

1. 找到 left、mid、right 的位置和关系,然后调用merge合并

2. 定义 gap 表示当前的分组是每组几个数据

    public static void mergeSortNor(int[] array) {
        int gap = 1;//每组几个数据
        while (gap < array.length) {
            for (int i = 0; i < array.length; i = i+gap*2) {
                int left = i;
                
                // mid、right 可能会越界
                int mid = left+gap-1;
                int right = mid+gap;

                if(mid >= array.length) {
                    mid = array.length-1;
                }
                if(right >= array.length) {
                    right = array.length-1;
                }
                merge(array,left,mid,right);
            }
            gap*=2;
        }
    

13、排序类算法总结

14、计数排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中

具体实现:

1.申请一个数组count,大小为待排序数组array的范围 M

2.遍历待排序数组array,把数字对应的count数组的下标内容进行++

3.遍历计数数组count 写回到待排序数组array,此时需要注意写的次数和元素值要一样

4. 最后数组array中存储的就是有序的序列

    public static void countSort(int[] array) {
        //求数组的最大值 和 最小值  O(N)
        int minVal = array[0];
        int maxVal = array[0];
        for (int i = 1; i < array.length; i++) {
            if(array[i] < minVal) {
                minVal = array[i];
            }
            if(array[i] > maxVal) {
                maxVal = array[i];
            }
        }
        //确定计数数组的 长度
        int len = maxVal - minVal + 1;
        int[] count = new int[len];
        //遍历array数组 把数据出现的次数存储到计数数组当中   O(N)
        for (int i = 0; i < array.length; i++) {
            count[array[i]-minVal]++;
        }
        //计数数组已经存放了每个数据出现的次数
        //遍历计数数组 把实际的数据写回array数组  O(M) M表示数据范围
        int index = 0;
        for (int i = 0; i < count.length; i++) {
            while (count[i] > 0) {
                //这里需要重写写回array 意味着得从array的0位置开始写
                array[index] = i+minVal;
                index++;
                count[i]--;
            }
        }
    }

计数排序的特性总结
1.计数排序是非基于比较的排序

2. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限;计数排序的场景:指定范围内的数据

3. 时间复杂度:O(MAX(N,M))   M表示数据范围
4. 空间复杂度:O(M)
5. 稳定性:稳定;上述代码是不稳定的写法

15、其他排序

15.1、基数排序

基数排序(Radix Sort)是一种非比较型的排序算法,它通过逐位比较元素的每一位(从最低位到最高位)来实现排序。基数排序的核心思想是将整数按位数切割成不同的数字,然后按每个位数分别进行排序。基数排序的时间复杂度为 O(d*(n+r)),其中 n 为元素个数,d 是最大数字的位数,r 为基数(桶的个数)

时间复杂度分析:

分配依次将每个数放到对应的桶中 O(n)

收集依次将每个桶里的元素拿出来 O(r)  (桶里的元素是用链表连接的)

每轮:分配+收集 O(n+r)

如果最大的数字有d位,就需要排d轮

所以时间复杂度为:O(d*(n+r))

15.2、桶排序

算法思想:划分多个范围相同的区间,每个子区间自排序,最后合并

计数排序、基数排序、桶排序 都是非基于比较的排序

相关文章:

  • 【C++】C++的引用
  • 在 Ubuntu 下通过 Docker 部署 Caddy 服务器
  • C++双链表介绍及实现
  • 从输入URL到页面渲染:浏览器请求的完整旅程解析
  • LLM学习笔记3——使用Docker(vLLM+OpenWebUI)实现本地部署DeepSeek-R1-32B模型
  • 基于HASM模型的高精度建模matlab仿真
  • Go 跨域中间件实现指南:优雅解决 CORS 问题
  • 十五、C++速通秘籍—异常处理
  • 基于Python的经济循环模型构建与可视化案例
  • Matlab添加标题title与标签lable
  • 上层 Makefile 控制下层 Makefile 的方法
  • 解释型语言和编译型语言的区别
  • 安全岗の夺命连环问:(第壹篇)从XSS到0day的灵魂拷问
  • 舵机、震动传感器、超声波使用代码
  • Qt 5.14.2 入门(四)菜单栏和工具栏的创建
  • 六、继承(三)
  • 如何用finallshell连接虚拟机
  • 前端下载文件时浏览器右上角没有保存弹窗及显示进度,下载完之后才会显示保存弹窗的问题定位及解决方案
  • PHP 拆词搜索(常用于搜索内容)
  • 从三次方程到复平面:复数概念的奇妙演进(一)
  • 外交部:习近平主席同普京总统达成许多新的重要共识
  • 复旦发文缅怀文科杰出教授裘锡圭:曾提出治学需具备三种精神
  • 胖东来发布和田玉、翡翠退货说明:不扣手续费等任何费用
  • 两部上戏学生作品亮相俄罗斯“国际大学生戏剧节”
  • 加力、攻坚、借力、问效,上海为优化营商环境推出增量举措
  • 美联储宣布维持基准利率不变