深入了解快速排序和归并排序

news2025/1/10 2:10:51

作者~小明学编程 

文章专栏Java数据结构

格言目之所及皆为回忆,心之所想皆为过往

快速排序和归并排序作为排序中的两个重点,也是面试中最常考的两个知识点,这里带大家详解的了解这两个排序。

目录

快速排序

原理

填坑法

代码

Hoare 法

优化

在待排序区间选择一个基准值

设置阈值

非递归法

性能

归并排序

原理

代码

递归法

非递归法

性能

总结对比


快速排序

原理

快速排序的思想是这样的:

填坑法

1.首先我们需要从待排序区间选择一个数,作为基准值(pivot),这里我们常用的就是那我们的最左边的数当作基准,将其挖走。

 2. Partition: 遍历整个待排序区间,我们将左边下标的位置定义为start,将右边下标的位置记作end,因为我们的最左边被挖走了所以我们要想办法将这个坑给填了,先是从右边向左找也就是end的位置开始找,找一个比基准(pivot)小的数然后将其填在我们的start的位置。

 接着我们从左向右找也就是start的位置开始找,找一个比基准(pivot)大的数将其填在end的坑中,

然后循环上述的过程直至start==end,

然后我们在strat的位置将我们的基准pivot给填上去。

 

 这时我们可以看到pivot的左边的数都小于基准,右边的数都大于基准,就完成了我们初步的排序。

3.采用分治思想,对基准的左右两个小区间按照同样的方式处理,直到start>end。

代码

    public static void quickSort(int[] array) {
        quick(array,0, array.length-1);
    }
    public static void quick(int[] array,int left,int right) {
        if (left>right) {
            return;
        }
        int pivot = partition(array,left,right);
        quick(array,left,pivot-1);
        quick(array,pivot+1,right);
    }
    public static int partition(int[] array,int start,int end) {
        int pivot = array[start];
        while (start<end) {
            while (array[end]>=pivot && start<end) {
                end--;
            }
            array[start] = array[end];
            while (array[start]<=pivot && start<end) {
                start++;
            }
            array[end] = array[start];
        }
        array[start] = pivot;
        return start;
    }

Hoare 法

Hoare 法的思想是:

1.首先我们需要从待排序区间选择一个数,作为基准值(pivot),这里我们常用的就是那我们的最左边的数当作基准。

 2.我们从左向右找一个比pivot大的数,然后再从右向左找一个比pivot小的数,

 接着交换这两个数,

 然后重复这个过程直至start==end,

 然后就是交换pivot位置的值和start位置的值,

 至此,pivot左边的值都小于pivot,右边的值都大于pivot。

3.采用分治思想,对基准的左右两个小区间按照同样的方式处理,直到start>end。

优化

先看一段代码

    public static void test1(int capacity) {
        int[] array = new int[capacity];
        Random random = new Random();
        for (int i = 0; i < capacity; i++) {
            array[i] = i;
        }
        long start = System.currentTimeMillis();
        quickSort(array);
        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }
    public static void main(String[] args) {
        test1(100000);
    }

这里我们给了十万个有序的数据进行排序,看看能不能完成我们的排序。

当我们运行代码的时候会发现报错,说我们的栈溢出了。因为当我们的数据比较有序的时候就相当于一个单一的链表。然后就一直开辟栈的空间直到我们的栈溢出。

想要避免这种情况的发生就必须让我们的快速排序尽量的从中间分开形成尽可能的完全二叉树。这样可以大大降低我们的栈的开辟。

在待排序区间选择一个基准值

1. 选择左边或者右边
2. 随机选取
3. 几数取中法
常见的选基准法有这三种其中的第一种也是我们前面用的那种,这种方法有较大的缺陷当我们的待排序列非常有序或者倒序的时候会非常的慢,然后就是随机选取法这种方法比较看脸因人而异也不是很建议,最后就是我们的几数随机取中法,下面给大家介绍。

我们现在就需要写一个方法找出一个尽量中间的数来作为我们的基准。

思路:

我们每次进行递归的时候会传入我们最左边和最右边两个数,然后我们取这两个数的中间下标,然后比较这三个数的大小,找到中间值,然后返回其下标,最后将当前的下标和我们的left的下标做交换。

代码

    public static void quick(int[] array,int left,int right) {
        if (left>right) {
            return;
        }
        int midIndex = findMidIndex(array,left,right);
        swap(array,left,midIndex);
        int pivot = partition(array,left,right);
        quick(array,left,pivot-1);
        quick(array,pivot+1,right);
    }
    public static int findMidIndex(int[] array,int start,int end) {
        int minIndex = start+(end-start)/2;
        if (array[start]<array[end]) {
            if (array[minIndex]<array[start]) {
                return start;
            } else if (array[minIndex]<array[end]) {
                return minIndex;
            } else {
                return end;
            }
        } else {
            if (array[minIndex]<array[end]) {
                return end;
            } else if (array[minIndex]<array[start]) {
                return minIndex;
            } else {
                return start;
            }
        }
    }

 

可以看到即使我们的数据上升到了十万级这个量级仍然二十几毫秒就排好序了,反而数据越有序排的越快,将我们的缺点变成了优点。

设置阈值

我们知道当我们的快速排序进行到后期的时候我们的待排序列已经基本上趋于有序了,我们还知道我们的插入排序是越有序,排的就越快,所以现在我们有一个想法,那就是设置一个阈值,当我们快速排序的待排序列低于这个阈值的时候,我们就采用我们的插入排序,这样也能进一步的减少时间。

代码

    public static void quick(int[] array,int left,int right) {
        if (left>right) {
            return;
        }
        //当我们的待排序个数小于1000的时候我们改为使用插入排序
        if (left+right+1<1000) {
            insertSort2(array, left, right);
        }
        //找到我们的中间基准
        int midIndex = findMidIndex(array,left,right);
        swap(array,left,midIndex);
        int pivot = partition(array,left,right);
        quick(array,left,pivot-1);
        quick(array,pivot+1,right);
    }
    //针对快速排序而制定的插入排序
    public static void insertSort2(int[] array,int left,int right) {
        for (int i = left+1; i <= right ; i++) {
            int temp = array[i];
            int j = i-1;
            for (; j >= left ; j--) {
                if (array[j] > temp) {
                    array[j+1] = array[j];
                } else {
                    break;
                }
            }
            array[j+1] = temp;
        }
    }

非递归法

其思想是先将我们的right和left放入栈中,

 接着出栈,分别将left和right赋值,然后进行排序返回我们的基准值的下标,然后分别将right和pivot+1还有pivot-1和left给压栈,接着重复当前的过程知道栈为空或者left>=right,当然在这期间也可以加入我们的优化。

代码

    //快速排序非递归
    public static void quickSort1(int[] array) {
        Stack<Integer> stack = new Stack<>();
        int left = 0;
        int right = array.length-1;
        //先将最右和最左放入栈中
        stack.push(right);
        stack.push(left);
        while (!stack.empty()) {
            //弹出我们的左右两个位置的下标
            left = stack.pop();
            right = stack.pop();
            //当我们的左下标大于等于右下标的时候则跳过当前的排序
            if (left>=right) {
                continue;
            }
            int midIndex = findMidIndex(array,left,right);
            swap(array,left,midIndex);
            int pivot = partition(array,left,right);
            stack.push(right);
            stack.push(pivot+1);
            stack.push(pivot-1);
            stack.push(left);
        }
    }

性能

归并排序

原理

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

简单的来说就是将我们待排序的序列一次分成两份,然后将两份再分成四份···,最后分成一份只有一个元素,然后再合并合并的时候因为要合并的两个已经是有序了,所以就转换成合并两个有序数组的问题了。

代码

递归法

递归法写起来比较容易理解,就是将我们原数组分成两部分,我们取一个中间值,先递归左边的部分再递归右边的部分,最后写好我们的额合并方法就行了。

    //归并排序(递归)
    public static void mergeSort(int[] array) {
        mergeSortInternal(array,0, array.length-1);
    }
    private static void mergeSortInternal(int[] array,int low,int high) {
        if (low>=high) {
            return;
        }
        int mid = low + (high-low) / 2;
        mergeSortInternal(array,low,mid);
        mergeSortInternal(array,mid+1,high);
        //合并
        merge(array,low,mid,high);
    }
    //合并两个有序的数组
    private static void merge(int[] array,int low,int mid,int high) {
        int newLength = high - low + 1;
        int[] newArray = new int[newLength];
        int newArrayIndex = 0;//新数组的下标
        int start1 = low;//数组1的开始
        int end1 = mid;//数组1的结尾
        int start2 = mid+1;//数组2的开始
        int end2 = high;//数组2的结尾
        //循环直到其中一个数组遍历完
        while (start1<=end1 && start2<=end2) {
            if (array[start1]<array[start2]) {
                newArray[newArrayIndex++] = array[start1++];
            } else {
                newArray[newArrayIndex++] = array[start2++];
            }
        }
        //将没遍历完的数组剩余的元素给到新数组中
        while (start2<=end2) {
            newArray[newArrayIndex++] = array[start2++];
        }
        while (start1<=end1) {
            newArray[newArrayIndex++] = array[start1++];
        }
        //拷贝当前数组到原数组中
        for (int i = 0; i < newLength; i++) {
            array[i+low] = newArray[i];
        }
    }

非递归法

非递归法写起来就相对的繁琐一点,但是代码却不繁琐,想明白了就很简单。

首先我们定义一个gap表示我们每组的元素个数,我们的i表示我们两个组的首位置,因为我们要合并两个组的数据(当合并只有一组的时候我们merge会直接return)。

left = i,mid = i + gap - 1,right = mid + gap。

    //归并排序(非递归)
    public static void mergeSort1(int[] array) {
        int gap = 1;//每组的元素个数
        while (gap< array.length) {
            for (int i = 0; i < array.length; i += gap*2) {
                int left = i;
                int mid = i + gap - 1;
                if (mid >= array.length) {
                    mid = array.length-1;
                }
                int right = mid + gap;
                if (right >= array.length) {
                    right = array.length-1;
                }
                merge(array,left,mid,right);
            }
            gap *= 2;
        }
    }

性能

时间复杂度空间复杂度
O(n * log(n))O(n)
数据不敏感数据不敏感

总结对比

排序方法最好平均最坏空间复杂度稳定性
冒泡排序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)稳定

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

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

相关文章

DSPE-PEG-TPP;磷脂-聚乙二醇-磷酸三苯酯;(阻燃剂TPP)是种含磷元素的化合物,可用作无卤环保型阻燃剂

中文名称&#xff1a; 二硬脂酰基磷脂酰乙醇胺-聚乙二醇-磷酸三苯酯&#xff1b;三苯基磷聚乙二醇磷脂 英文简称&#xff1a; DSPE-PEG-TPP,TPP-PEG-DSPE 分子量&#xff1a; 2000,3400,5000等 溶剂: 溶于部分有机溶剂 磷酸三苯酯为无味、无臭的白色结…

JDK8 连接Access数据库

JDK8 连接Access数据库1. 安装JDK82. 下载配置文件3. 源码设置前面我们讲了如何使用Java连接ODBC并配置Access数据库&#xff0c; 参考连接&#xff1a;https://jackwei.blog.csdn.net/article/details/86285822 可以知道JDK8之后已经不支持jdbc-odbc桥接了&#xff0c;如果你可…

windows10上运行magic keyboard和magic mouse

windows10上运行magic keyboard和magic mouse并保持你的mac习惯 所有需要的软件和插件都可以在这里寻找到链接&#xff1a;https://pan.baidu.com/s/1Y8vjRnznqKP7f8dFFrHoGw?pwdvpsy 提取码&#xff1a;vpsy 安装蓝牙 你的windows电脑可能自带了蓝牙&#xff0c;那你直接…

保姆级教程带你从0到1实现基于bitcask的kv存储引擎

愿景 ​ 今年大部分业余时间都在nutsdb的开源贡献上&#xff0c;nutsdb是基于bitcask模型实现的持久化存储引擎&#xff0c;提供了诸如list&#xff0c;set等多种丰富的数据结构。近来很多小伙伴&#xff0c;其中也有一些我的好朋友陆陆续续加入到这个项目上来。为了帮助小伙伴…

tensorflow2 SqueezeNet

前面学习了通过加深网络和加宽网络来改进模型质量&#xff0c;提高模型精度的深度学习backbone模型&#xff08;LeNet,VGGNet,AlexNet,GoogleNet,ResNet),这里介绍如何使网络更快&#xff0c;结构更轻量化的改进深度神经网络模型之一————SqueezeNet&#xff0c;它能够在Ima…

【JavaWeb】文件的上传和下载

文章目录一.文件的上传介绍⭐️1.文件上传及HTTP协议的说明2.commons-fileupload.jar常用API介绍说明二.文件下载⭐️一.文件的上传介绍⭐️ 1.文件上传及HTTP协议的说明 (1).要有一个form标签,methodpost请求 (2).form标签的encType属性值必须为multipart/form-data值 (3).在…

VTK在Windows上的安装

本章介绍在计算机系统上安装VTK。在Microsoft Windows上&#xff0c;可以安装预编译的vtk.exe&#xff0c;也可以从源码自行编译vtk软件。您可能希望了解系统架构&#xff0c;阅读会使编译过程更容易跟踪。如果遇到问题&#xff0c;可以联系vtkusers邮件列表。 2.1 概述 VTK在…

【HTML + CSS】笔记

页面设计 1.HTML&#xff1a;结构框架 2.CSS 3.JS HTML&#xff1a;超文本标记语言 <...>&#xff1a;标签/元素 <!DOCTYPE html>&#xff1a;解释文档类型为html head区域常用标签 <base> 使用后浏览器不再使用当前文档的URL&#xff0c;而使用指定的…

web前端设计与开发期末作品/期末大作业-疫情

Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业&#xff0c;击疫情致敬逆行者感人类题材 | 致敬逆行者网页设计作品 | 大学生抗疫感动专题网页设计作业模板 | 等网站的设计与制作 | HTML期末大学生网页设计作业 HTML&#xff1a…

Windows之应用安装程序 —— winget

大家都用过Linux中的应用程序安装工具&#xff0c;如yum、apt、rpm等工具进行安装自己想要的一些工具或则软件之类的&#xff0c;当然Linux操作系统还是很强大的有很多类似的命令来安装我们所需要的程序&#xff0c;但是windwos有没有类似于windows这样的应用安装程序呢&#x…

可解释的AI:用LIME解释扑克游戏

可解释的AI&#xff08;XAI&#xff09;一直是人们研究的一个方向&#xff0c;在这篇文章中&#xff0c;我们将看到如何使用LIME来解释一个模型是如何学习扑克规则的。在这个过程中&#xff0c;我们将介绍: 如何将LIME应用到扑克游戏中;LIME如何工作;LIME 的优点和缺点是什么。…

免费查题系统搭建

免费查题系统搭建 本平台优点&#xff1a; 多题库查题、独立后台、响应速度快、全网平台可查、功能最全&#xff01; 1.想要给自己的公众号获得查题接口&#xff0c;只需要两步&#xff01; 2.题库&#xff1a; 查题校园题库&#xff1a;查题校园题库后台&#xff08;点击跳…

STM32存储器组织-STM32存储器映像-嵌入式SRAM-STM32位段-嵌入式闪存-STM32启动配置

STM使用说明第二篇【1】STM32存储器组织【2】STM32存储器映像【3】嵌入式SRAM【4】STM32位段【5】嵌入式闪存【6】STM32启动配置【1】STM32存储器组织 程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。 数据字节以小端格式存放在存储器中。一…

【Java】IO流 - 节点流和处理流【Buffered】

文章目录节点流和处理流BufferedReaderBufferedWriterBufferd拷贝BufferedInputStream/BufferedOutputStream节点流和处理流 数据源就是存放数据的地方&#xff0c;可以是 文件、数组 等等&#xff1b; 节点流是比较底层的&#xff0c;直接操作二进制数据 包装流&#xff08;处…

石英砂过滤器 多介质过滤器 活性炭过滤器

石英砂过滤器简介 石英砂过滤器属于机械过滤器的一种为压力式过滤器&#xff0c;采用ABS蘑菇型水帽布水&#xff0c;内装若干种规格精制石英砂滤料&#xff0c;阻力小&#xff0c;通量大。利用过滤器内所装的填料来截留去除水中悬浮微粒和胶体杂质。当过滤器因滤层污脏&#x…

解决git配置多个SSH公钥的问题

项目场景&#xff1a; 之前跟同事共用一个项目私密仓库&#xff0c;现需拆分成两个仓库&#xff0c;结果同事提出他不想换&#xff0c;让我这边再创建一个新仓库。 那么接下来就遇到一个情况&#xff0c;原来仓库的公钥我并不想删除&#xff0c;还想继续使用&#xff0c…

Ubuntu20.04沉浸式装机

Ubuntu20.04沉浸式装机 文章目录Ubuntu20.04沉浸式装机前言1.装机之后系统更新&#xff0c;设置软件源2.安装系统驱动Notice3 安装CUDA及CuDNN4 常用软件安装4.1 常用软件安装4.2 Typora安装4.3 docker安装4.4 nvidia-docker 安装4.5 pypcd 安装4.6 PCL安装环境安装4.7 Eigen安…

Python学习基础笔记七——元组

元组tuple&#xff0c;跟列表相似&#xff0c;元组不能在原处修改。元组不支持任何方法调用&#xff0c;但是元组具有列表的大多数属性。 但是要记住的是&#xff1a;元组的不可变性只适用于元组本身&#xff0c;并非其内容。例如元组内部的列表是可以像往常一样修改的。 元组常…

Go1.20 arena新特性示例详解

当时我们还想着 Go 团队应该不会接纳&#xff0c;至少不会那么快&#xff1a; 懒得翻也可以看我再次道来&#xff0c;本文提到的提案《proposal: arena: new package providing memory arenas》&#xff0c;这其中的 Arena 将会是一个突破项。 快速背景 Arena 指的是一种从一个…

Java接口的应用

目标&#xff1a;总结Comparable接口以及compareTo方法、comparator接口中compare方法比较器、toString方法、equals方法、hashCode方法、Cloneable接口以及深浅拷贝 比较对象中内容的大小【Comparable接口以及compareTo方法】 例如&#xff1a;学生类&#xff1a;成员有姓名、…