【Java基础】——Java排序(下)

简介: 【Java基础】——Java排序(下)

6、快速排序(Quick Sort)


1. 基本算法


归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;

快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。


af8d49a2b0804f3bb9fda6a9148ba1d1.png

public class QuickSort<T extends Comparable<T>> extends Sort<T> {
    @Override
    public void sort(T[] nums) {
        shuffle(nums);
        sort(nums, 0, nums.length - 1);
    } 
    private void sort(T[] nums, int l, int h) {
        if (h <= l)
            return;
        int j = partition(nums, l, h);
        sort(nums, l, j - 1);
        sort(nums, j + 1, h);
    } 
    private void shuffle(T[] nums) {
        List<Comparable> list = Arrays.asList(nums);
        Collections.shuffle(list);
        list.toArray(nums);
    }
}


2. 切分


取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描,找到第一个小于等于它的元素,交换这两个元素,并不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和 a[j] 交换位置。


3c598824b7fc4d16a2c3e8da41d699d9.png

private int partition(T[] nums, int l, int h) {
        int i = l, j = h + 1;
        T v = nums[l];
        while (true) {
            while (less(nums[++i], v) && i != h) ;
            while (less(v, nums[--j]) && j != l) ;
            if (i >= j)
                break;
            swap(nums, i, j);
        } 
        swap(nums, l, j);
        return j;
    }


3. 性能分析


快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。

快速排序最好的情况下是每次都正好能将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为CN=2CN/2+N,复杂度为 O(NlogN)。

最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。


4. 算法改进


(一)切换到插入排序

因为快速排序在小数组中也会递归调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以

切换到插入排序。

(二)三数取中

最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大

小居中的元素作为切分元素的效果最好。

(三)三向切分

对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。三向切分快速排序对于只有若干不同主键的随机数组可以在线性时间内完成排序。


public class ThreeWayQuickSort<T extends Comparable<T>> extends QuickSort<T> {
    @Override
    protected void sort(T[] nums, int l, int h) {
        if (h <= l)
            return;
        int lt = l, i = l + 1, gt = h;
        T v = nums[l];
        while (i <= gt) {
            int cmp = nums[i].compareTo(v);
            if (cmp < 0)
                swap(nums, lt++, i++);
            else if (cmp > 0)
                swap(nums, i, gt--);
            else
                i++;
        } 
        sort(nums, l, lt - 1);
        sort(nums, gt + 1, h);
    }
}


5. 基于切分的快速选择算法


快速排序的 partition() 方法,会返回一个整数 j 使得 a[l…j-1] 小于等于 a[j],且 a[j+1…h] 大于等于 a[j],此时 a[j]就是数组的第 j 大元素。

可以利用这个特性找出数组的第 k 个元素。


public T select(T[] nums, int k) {
        int l = 0, h = nums.length - 1;
        while (h > l) {
        int j = partition(nums, l, h);
        if (j == k)
        return nums[k];
        else if (j > k)
        h = j - 1;
        else
        l = j + 1;
        } 
        return nums[k];
        }


该算法是线性级别的。因为每次能将数组二分,那么比较的总次数为 (N+N/2+N/4+…),直到找到第 k 个元素,这个和显然小于 2N。


7、堆排序(Heap Sort)

202105161702205.gif


第一步:构建初始堆buildHeap, 使用sink(arr,i, length)调整堆顶的值;

第二步:将堆顶元素下沉 目的是将最大的元素浮到堆顶来,然后使用sink(arr, 0,length)调整;

1. 堆

堆的某个节点的值总是大于等于子节点的值,并且堆是一颗完全二叉树。

堆可以用数组来表示,因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。


2b4121d0a7fd41db89aafd9dbc8b952c.png

public class Heap<T extends Comparable<T>> {
    private T[] heap;
    private int N = 0;
    public Heap(int maxN) {
        this.heap = (T[]) new Comparable[maxN + 1];
    } 
    public boolean isEmpty() {
        return N == 0;
    } 
    public int size() {
        return N;
    } 
    private boolean less(int i, int j) {
        return heap[i].compareTo(heap[j]) < 0;
    } 
    private void swap(int i, int j) {
        T t = heap[i];
        heap[i] = heap[j];
        heap[j] = t;
    }
}


2. 上浮和下沉


在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作,把这种操作称为上浮。

e4c63799f4e74e8c9c8ac7de1522da17.png

 
         
private void swim(int k) {
        while (k > 1 && less(k / 2, k)) {
            swap(k / 2, k);
            k = k / 2;
        }
    }


类似地,当一个节点比子节点来得小,也需要不断地向下进行比较和交换操作,把这种操作称为下沉。一个节点如果有两个子节点,应当与两个子节点中最大那么节点进行交换。


cd5acc0318084a7d87c5c5e95835a750.png

private void sink(int k) {
        while (2 * k <= N) {
          int j = 2 * k;
        if (j < N && less(j, j + 1))
          j++;
        if (!less(k, j))
          break;
        swap(k, j);
        k = j;
        }
        }


3. 插入元素


将新元素放到数组末尾,然后上浮到合适的位置。


public void insert(Comparable v) {
  heap[++N] = v;
  swim(N);
}


4. 删除最大元素


从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。


public T delMax() {
  T max = heap[1];
  swap(1, N--);
  heap[N + 1] = null;
  sink(1);
  return max;
}

5. 堆排序


由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序。并且堆排序是原地排序,不占用额外空间。

(一)构建堆

无序数组建立堆最直接的方法是从左到右遍历数组,然后进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。


f57f5c469a6e426e996f9dee27584faf.png

(二)交换堆顶元素与最后一个元素

交换之后需要进行下沉操作维持堆的有序状态。

a17e8d60528d48babdfee7c06eb90beb.png

public class HeapSort<T extends Comparable<T>> extends Sort<T> {
    /**
     * 数组第 0 个位置不能有元素
     */
    @Override
    public void sort(T[] nums) {
        int N = nums.length - 1;
        for (int k = N / 2; k >= 1; k--)
            sink(nums, k, N);
        while (N > 1) {
            swap(nums, 1, N--);
            sink(nums, 1, N);
        }
    } 
    private void sink(T[] nums, int k, int N) {
        while (2 * k <= N) {
            int j = 2 * k;
            if (j < N && less(nums, j, j + 1))
                j++;
            if (!less(nums, k, j))
                break;
            swap(nums, k, j);
            k = j;
        }
    } 
    private boolean less(T[] nums, int i, int j) {
        return nums[i].compareTo(nums[j]) < 0;
    }
}


6. 分析


一个堆的高度为 logN,因此在堆中插入元素和删除最大元素的复杂度都为 logN。

对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlogN。

堆排序时一种原地排序,没有利用额外的空间。

现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较。


小结


1. 排序算法的比较

bfd9154973df48ef901cd602595dfe58.png


快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 ~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。


2. Java 的排序算法实现


Java 主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。


3、其他排序


基数排序:基数排序是用空间换时间的经典算法,当数据足够多时,达到几千万级别时内存空间可能不够用了,发生堆内存溢出。

计数排序 (Count Sort)

桶排序(Bucket Sort):桶排序可以看成是计数排序的升级版,它将要排的数据分到多个有序的桶里,每个桶里的数据再单独排序,再把每个桶的数据依次取出,即可完成排序。

时间复杂度如下:


6ad555dd462147d39f106c9ea39193d2.png


目录
相关文章
|
2月前
|
Java Linux
java基础(3)安装好JDK后使用javac.exe编译java文件、java.exe运行编译好的类
本文介绍了如何在安装JDK后使用`javac.exe`编译Java文件,以及使用`java.exe`运行编译好的类文件。涵盖了JDK的安装、环境变量配置、编写Java程序、使用命令行编译和运行程序的步骤,并提供了解决中文乱码的方法。
61 2
|
14天前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
415 37
|
1月前
|
前端开发 小程序 Java
java基础:map遍历使用;java使用 Patten 和Matches 进行正则匹配;后端传到前端展示图片三种情况,并保存到手机
这篇文章介绍了Java中Map的遍历方法、使用Pattern和matches进行正则表达式匹配,以及后端向前端传输图片并保存到手机的三种情况。
20 1
|
1月前
|
存储 搜索推荐 算法
【用Java学习数据结构系列】七大排序要悄咪咪的学(直接插入,希尔,归并,选择,堆排,冒泡,快排)以及计数排序(非比较排序)
【用Java学习数据结构系列】七大排序要悄咪咪的学(直接插入,希尔,归并,选择,堆排,冒泡,快排)以及计数排序(非比较排序)
23 1
|
1月前
|
Oracle Java 关系型数据库
|
2月前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
2月前
|
缓存 安全 Java
【Java面试题汇总】Java基础篇——基础、修饰符和关键字(2023版)
Java的特点和优点,、Java 8的新特性、面向对象、基本数据类型和引用类型、自动拆装箱与自动装箱、==与equals()的区别、为什么重写equals()就要重写hashcode()、抽象类和接口的区别、重载和重写的区别、四种引用方式、wt()和sleep()的区别、java方法是值传递还是引用传递?访问修饰符、static、final、this和super、volatile的用法及原理
【Java面试题汇总】Java基础篇——基础、修饰符和关键字(2023版)
|
3月前
|
存储 Java
Java中ArrayList 元素的排序
本文提供了Java中根据`ArrayList`元素的某个属性进行排序的示例代码,包括实现`Comparable`接口和重载`compareTo`方法,然后使用`Collections.sort`方法进行排序。
|
3月前
|
存储 Java API
【Java高手必备】揭秘!如何优雅地对List进行排序?掌握这几种技巧,让你的代码瞬间高大上!
【8月更文挑战第23天】本文深入探讨了Java中对List集合进行排序的各种方法,包括使用Collections.sort()、自定义Comparator以及Java 8的Stream API。通过示例代码展示了不同情况下如何选择合适的方法:从简单的整数排序到自定义类对象的排序,再到利用Comparator指定特殊排序规则,最后介绍了Stream API在排序操作中的简洁应用。理解这些技术的区别与应用场景有助于提高编程效率。
77 4