数据结构之八大排序(下)

news2024/11/15 12:36:42

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:数据结构(Java版) 

数据结构之八大排序(上)-CSDN博客 

上面博客讲述了另外六中排序算法。

目录

快速排序 

归并排序 


快速排序 

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

总共有三个实现版本:

1、Hoare法版:

思路:取key作为基准,右边找比基准小的,左边找比基准大的,进行交换,一直重复上述步骤,直至两者相遇,相遇后再交换相遇值和基准值,接着就以相遇的值为界,划分为左右两边,继续重复上述步骤。

代码实现: 

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

    private static void quick_sort(int[] array, int start, int end) {
        // 限制条件
        if (start >= end) {
            return;
        }
        // 分区并进行交换
        int div = partion(array, start, end);
        // 递归左子树
        quick_sort(array, start, div-1);
        // 递归右子树
        quick_sort(array, div+1, end);
    }

    private static int partion(int[] array, int left, int right) {
        int key = array[left]; // 指定基准
        int keyIndex = left; // 记录基准的下标,用于交换
        while (left < right) {
            // 右边找到比基准小的
            while (left < right && array[right] >= key) {
                right--;
            }
            // 左边找到比基准大的
            while (left < right && array[left] <= key) {
                left++;
            }
            swap(array, left, right);
        }
        // 相遇后交换
        swap(array, left, keyIndex);
        // 返回相遇点
        return left;
    }

注意: 

1、限制条件。我们在递归的时候,如果只有一个元素时, 说明此时一定是有序的,就不需要再继续进行递归了。即 start == end。如果在递归过程中 start > end了说明也不需要交换了。

2、在左右找比基准大或者小时,一定要先找右边,再找左边。否则达不到我们的目的(左边全小于边界值,右边全大于边界值)。如下图所示:

这样再去交换相遇值和基准值就不能实现相遇值(边界值)全大于其左边,小于其右边。

3、相遇交换基准值和相遇值时,一定要先把基准值对应的下标储存起来。

4、时间复杂度:最坏情况:O(N^2):当数据已经有序的情况下;最好情况:O(N*logN):数据刚好可以组成满二叉树,每次都是N个数据,递归了logN层,即N*logN。

5、空间复杂度:最坏情况:O(N):当数据已经有序的情况下;最好情况:O(logN):数据刚好可以组成满二叉树,递归了logN层。

6、稳定性:不稳定。

2、挖坑法版: 

思路:同样是取左边的第一个数据为 key,形成一个坑位,在右边找到比key小的放到左边的坑位中(这就意味着右边形成了坑位),再从左边找到比 key 大的放到坑位中。一直重复上面的操作直至两者相遇,就把 key 放到坑位中,那么这个坑位(相遇点)就是划分标准,然后我们继续从两棵子树开始上述操作。

代码实现:

    private static int partion(int[] array, int left, int right) {
        int key = array[left];
        while (left < right) {
            // 右边找小于坑的值
            while (left < right && array[right] >= key) {
                right--;
            }
            // 开始填坑
            array[left] = array[right];
            // 左边找大于坑的值
            while (left < right && array[left] <= key) {
                left++;
            }
            // 继续填坑
            array[right] = array[left];
        }
        array[left] = key;
        return left;
    }

注意:整体代码和上述代码一致,只是改变了partion的代码部分,也就是划分标准改变了。 

3、前后指针法版:

思路:定义一个指针 prev 和一个指针 cur,分别指向 start 和 start+1 的位置,开始遍历数组,如果 cur指向的数据小于 left 指向的数据,并且prev指向的下一个数据不是 cur 指向的数据,那么就交换两者的值,并同时往后走;如果 cur指向的数据大于 left 指向的数据,就让 cur 继续往后走,直至cur 遇到end就停止。并交换prev和left的值。

代码实现:

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

上面遇到了是单分支的树的情况,就导致快速排序变得类似冒泡排序一样了。针对上述情况,我们就要来优化代码。

思路一:既然我们拿到的数组是单分支的情况,那么我们只要把这棵树的中间节点(就是类似这组数据的中位数)的给找出来,然后再交换0位置的值即可,这就铸造了最开始的情况(图中数据所示)。

找根节点这里用到的是:三数取中法。即得到 left 、left+right 和 right 的中位数。

代码实现:

    // 三数取中法找到key,使单分支的树接近满二叉树
    private static int getIndex(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 {
            if (array[mid] < array[right]) {
                return right;
            } else if (array[mid] > array[left]) {
                return left;
            } else {
                return mid;
            }
        }
    }

注意:这里得到的只是一个近似值,不一定是准确的中位数。

思路二:二叉树的节点主要是集中在接近叶子结点的层数。既然如此,那直接把接近叶子节点的层树的节点全部采用直接插入排序即可。

直接插入排序部分的代码实现:

    private static void insertSortRange(int[] array, int start, int end) {
        for (int i = start+1; i <= end; i++) {
            int tmp = array[i];
            int j = i-1;
            while (j >= 0) {
                if (array[j] > tmp) {
                    array[j+1] = array[j];
                    j--;
                } else {
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

核心代码部分:

// 递归到接近叶子结点时,采用直接插入排序更有效
if (end - start <= 7) { //这个数是随机取的,只要是接近叶子节点层树即可
    insertSortRange(array, start, end);
    return;
}

前面都是递归实现的代码,下面我们就来学习使用迭代的方式实现快速排序:

    private static void quickSortNor(int[] array, int start, int end) {
        Stack<Integer> stack = new Stack<>();
        stack.push(start);
        stack.push(end);
        while (!stack.isEmpty()) {
            end = stack.pop();
            start = stack.pop();
            int div = partion(array, start, end);
            // 判断是否区间中是否只剩下一个元素
            if (div > start+1) {
                stack.push(start);
                stack.push(div-1);
            }
            if (div < end-1) {
                stack.push(div+1);
                stack.push(end);
            }
        }
    }

 其实非递归也就是大体结构发生了变化,在核心代码部分还是并未发生变化(交换数据)。

总结:虽然快速排序在不优化的情况下,时间复杂度确实能够达到O(N^2),但是我们平时都是说优化情况下的时间复杂度,即O(N*logN)。

归并排序 

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

思路:先将一组数据进行分解,分解成只有一组数据的时候,就可以开始合并了。

分解代码实现:

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

    private static void merge_sort(int[] array, int start, int end) {
        // 限制条件
        if (start >= end) {
            return;
        }
        int mid = (start+end)/2;
        // 分解
        merge_sort(array, start, mid); // 左子树
        merge_sort(array, mid+1, end); // 右子树
        // 合并(类似合并两个有序数组)
        merge(array, start, mid, end);
    }

合并的思路:类似合并两个数组。

代码实现:

    private static void merge(int[] array, int start, int mid, int end) {
        int s1 = start;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = end;
        // 申请一个新的数组来排序数据
        int[] tmp = new int[end-start+1];
        int k = 0;
        while (s1 <= e1 && s2 <= e2) {
            if (array[s1] < array[s2]) {
                tmp[k++] = array[s1++];
            } else {
                tmp[k++] = array[s2++];
            }
        }
        // 把剩下的数据全部放到数组中
        while (s1 <= e1) {
            tmp[k++] = array[s1++];
        }
        while (s2 <= e2) {
            tmp[k++] = array[s2++];
        }
        // 把排好序的数组数据存放到原数组中
        for (int i = start; i < start+k; i++) {
            array[i] = tmp[i-start];
        }
        /*// 或者写成下面这样
        for (int i = 0; i < k; i++) {
            array[i+start] = tmp[i];
        }*/
    }

注意:

1、时间复杂度:O(N*logN):每次进行分解的数据都是N个,总共会递归logN次,即N*logN。

2、空间复杂度:O(N):最后在合并的时候,会申请一个大小为N的数组,即N。

3、稳定性:稳定。既可以实现为稳定的排序,也可以实现为不稳定的排序。

同样归并排序也有非递归的实现方式。

思路:类似希尔排序的逆过程。就是把数据一个一个分成一组,使其有序,再把数据两个两个分成一组使其有序。直至一组的个数等于数据长度时,这个数据就有序了。

代码实现:

    public static void mergeSortNor(int[] array, int start, int end) {
        // 把数据每次分为两组,进行比较-排序
        int gap = 1;
        while (gap < array.length) {
            // 每次排序两组有效数据(因此i每次要跳过两组)
            for (int i = 0; i < array.length; i+=gap*2) {
                int left = i;
                int mid = left+gap-1;
                int right = mid+gap;
                // 要确保,mid和right是有效的
                if (mid >= array.length) {
                    mid = array.length-1;
                }
                if (right >= array.length) {
                    right = array.length-1;
                }
                merge(array, left, mid, right);
            }
            gap *= 2;
        }
    }

由于归并排序需要O(N)的空间复杂度,因此其主要是解决外部排序的问题。 

外部排序:排序过程需要在磁盘等外部存储进行的排序

前提:内存只有 1G,需要排序的数据有 100G  

因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序: 1. 先把文件切分成 200 份,每个 512 M

2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以

3. 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了。

注意:归并是下面这样处理的。

总结:常见的八大排序的情况:

这里的希尔排序可以是1.3-1.5,也可以是1-2。由于没有研究证明因此不能得出确切的答案。

好啦!本期 数据结构之八大排序(下)的学习之旅就到此结束啦!我们下一期再一起学习吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1975869.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

仓颉 -- 标识符 , 变量以及数据类型详解

仓颉 – 标识符 , 变量以及数据类型 一. 标识符 1. 普通标识符 由数字 , 字母 , 下划线构成 – cangjie , cangjie_2024由英文字母开头&#xff0c;后接零至多个英文字母、数字或下划线。由一至多个下划线开头&#xff0c;后接一个英文字母&#xff0c;最后可接零至多个英文…

phpMyAdmin 漏洞复现教程

一.登陆 账号密码 是数据库的 二.日志文件拿到shell 在sql里执行命令 可以看到是关闭状态 我们再次执行命令 让它变成on 日志文件开启 再次执行上面的命令 可以看到已经开启了 然后我们更改日志保存路径 然后查看是否更改成功 显示 更改成功 然后我们插入一句话木马 访问一下…

完成订单业务

文章目录 概要整体架构流程技术细节小结 概要 完成订单是电子商务、外卖平台、在线零售等多个行业中的一项重要业务流程。这项功能允许商家或平台将订单状态更新为“已完成”&#xff0c;表明订单已经成功交付给客户。 需求分析以及接口设计 技术细节 1.Controller层: ApiOp…

C#类和结构体的区别

1、类class是引用类型&#xff0c;多个引用类型变量的值会互相影响。存储在堆&#xff08;heap&#xff09;上 2、结构体struct是值类型&#xff0c;多个值类型变量的值不会互相影响。存储在栈&#xff08;stack&#xff09;上 类结构关键字classstruct类型引用类型值类型存储…

Study--Oracle-07-ASM故障组管理(六)

一、ORACLE ASM提供的三冗余方式 1、三种模式&#xff1a;external、normal、high 一般情况下三种模式需要的最小磁盘组&#xff1a; external 1块 normal 3块 high 5块 2、外部冗余&#xff08;external redundancy&#xff09; 表示Oracle不帮你管理镜像&#xf…

计算机网络-http协议和https的加密原理

HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;是用于在万维网&#xff08;World Wide Web&#xff09;上传输超文本的基础协议。它定义了客户端&#xff08;通常是浏览器&#xff09;和服务器之间的文本数据传输格式和规则。以下是HTTP的…

J030_TCP通信

一、需求描述 使用TCP协议进行通信 1.1 一发一收 1.1.1 Client package com.itheima.tcp1;import java.io.DataOutputStream; import java.io.OutputStream; import java.net.Socket;public class Client {public static void main(String[] args) throws Exception {//1、…

嵌入式初学-C语言-练习三

#部分题目可能在之前的博客中有&#xff0c;请谅解&#xff0c;保证常见题型均被发出# 1.计算n以内所有正奇数的和 ? n值通过键盘输入 代码&#xff1a; 1 /*2 需求&#xff1a;计算n以内所有正奇数的和 ? n值通过键盘输入3 */4 #include <stdio.h>5 6 int main()7 …

用手机剪辑视频素材从哪里找?用手机视频素材库分享

视频编辑是一门充满创意的艺术&#xff0c;无论是制作短片、广告还是个人Vlog&#xff0c;都离不开高质量的视频素材。如果自己拍摄的素材不能完全满足创作需求&#xff0c;或者需要更多样化的内容来丰富视频&#xff0c;那么优质的视频素材来源至关重要。下面推荐几个提供高品…

LinuxC++(8):GDB调试

下载gdb gdb需要使用yum下载 yum -y install gdb 编译注意 需要在后面加上 -g &#xff0c;证明是要给可调试文件。 开始调试 gdb函数名 修改主函数参数 set args //set args "小红" "小华" "爱你" 在linux中显示行号 在vi下&#xff0c;输入…

C++自定义接口类设计器之函数解析二

关键代码 // 解析为函数 bool FunctionCreator::parse(const QString& lineFunc) {auto trimFunc lineFunc.trimmed();auto list trimFunc.split(" ");bool bHasReturn false;// 返回值和函数名解析for (const auto& key : list) {auto trimKey key.trim…

麦田物语第十八天

系列文章目录 麦田物语第十八天 文章目录 系列文章目录一、(Editor)制作 [SceneName] Attribute 特性二、场景切换淡入淡出和动态 UI 显示一、(Editor)制作 [SceneName] Attribute 特性 在本节课我们编写Unity的特性Attribute来更好的完善我们项目,具体是什么呢,就是当…

一款简单且强大的免费开源图片压缩软件

图压是一款简单易用且功能强大的图片压缩工具&#xff0c;适用于Windows和macOS两大操作系统。它能够在几乎不损害图片清晰度的情况下&#xff0c;显著减小图片的体积&#xff0c;特别适合需要在网页、PPT、Word、PDF中使用的图片压缩。图压的操作界面简洁&#xff0c;用户可以…

Kettle同步数据时如何借助Shell通过SSH连接MySQL数据库

在实际开发中&#xff0c;经常会用到KettleSpoon来同步数据&#xff0c;比如&#xff1a;需要定时将MySQL库某张表前一天的数据同步到SQL Server&#xff08;MySQL&#xff09;库中等等。一般由于安全性都会提供基于秘钥的连接方式&#xff0c;这种情况下如何在Kettle中连接数据…

Wordpress建站问题记录

从一月到七月因为工作的情况没有进行太深入的开发,想着整理一下把做一个独立站把博客多个渠道发布一下,遇到几个问题在这里记录一下. 先写一下我的配置 系统: centos7 php: 7.4 wordpress: 6.6.1 mysql:8.0.6 1. HTTP 500 Internal 这个问题出现在我将wordpress的文件夹全部…

运维变革背景下的运维工具衍化探讨

在数字化转型的浪潮中&#xff0c;运维领域正经历着前所未有的变革。这一变革不仅重塑了业务形态&#xff0c;也对运维工具和运维组织模式产生了深远影响。随着基础设施云化、容器化、微服务化等技术的兴起&#xff0c;运维对象、运维流程、协同关系等各个方面都发生了深刻的变…

J.U.C 原子类之AtomicIntegerFieldUpdate

❃博主首页 &#xff1a; 「码到三十五」 &#xff0c;同名公众号 :「码到三十五」&#xff0c;wx号 : 「liwu0213」 ☠博主专栏 &#xff1a; <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关> ♝博主的话 &#xff1a…

MySQL:数据库用户

数据库用户 在关系型数据库管理系统中&#xff0c;数据库用户&#xff08;USER&#xff09;是指具有特定权限和访问权限的登录账户。每个用户都有自己的用户名和密码&#xff0c;以便系统可以通过认证来识别他们的身份。数据库用户可以登录数据库&#xff0c;在其中执行各种类…

第二十天学习笔记2024.8.2

安装mysql 1.安装软件包 centos7 中安装 mysql 8.x_wffkg-CSDN博客 2.解压 tar -xf mysql-8.0.33-1.el7.x86_64.rpm-bundle.tar 3.卸载mariadb yum remove -y *mariadb* 4.安装&#xff08;缺什么依赖补什么&#xff09; mysql-community-server-8.0.33-1.el7.x86_64.rpm 5.…

APP逆向 day26unidbg下-pdd(anti)案例

一.前言 今天我们讲unidbg的下篇&#xff0c;也就是unidbg基础的最后一个部分&#xff0c;我们上节课也有补环境&#xff0c;比如补java环境&#xff0c;补安卓环境&#xff0c;这节课我们讲的肯定比这些都要难&#xff0c;我会给出一个之前讲过的案例&#xff0c;然后会讲一个…