排序、JAVA版本的快排

本文深入探讨了三种经典的排序算法:快速排序、归并排序和插入排序。详细解析了它们的实现原理,并提供了具体的代码实现。同时,通过实例分析了在特定场景如寻找数组中的第K大元素、最小数字排列及计算逆序对等问题上,这些排序算法的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

快速排序

1.从序列中选择一个轴点元素pivot 假设每次选择索引为0的元素为轴点元素
2.利用pivot将序列分割成2个子序列:将小于pivot的元素放在pivot的前面(左边)
将大于pivot的元素放在pivot的后面(右边) 等于pivot的元素放在哪边都可以
3.对子序列进行上述1 2步骤 直到不能再分割(子序列中只剩下1个元素)

快速排序的本质:逐渐将每一个元素都转换成轴点元素

实现

  1. 假设数组为array={7,1,2,3,4,5,6} 最初pivot=7 进行一轮快速排序后array={6,1,2,3,4,5,7} 然后pivot=6 进行一轮快速排序后array={5,1,2,3,4,6,7}……显然 每次轴点元素都是数组中最大的元素 会对数组进行一次彻底的完全的遍历排序(也就是说不会出现左边元素小于轴点元素 不用操作 直接begin++ 或者右边元素大于轴点元素 不用操作 直接end-- 这两种情况)会降低排序性能 所以不能永远选索引为0的元素为轴点元素 为了降低最坏情况出现的概率 我们每次在数组[begin,end)范围中随机选一个数 把随机数和begin位置的元素进行交换 每次还是获取索引为0的元素为轴点元素
  2. 执行过程:先备份轴点元素 再用array[end]元素和轴点元素进行比较 若end元素大 则end-- 然后用array[end]元素和轴点元素进行比较 若轴点元素>=end元素 则把end元素放在begin位置 begin++ 此时调换比较方向 然后用array[begin]元素和轴点元素进行比较 若轴点元素大 则begin++ 然后用array[begin]元素和轴点元素进行比较 若轴点元素<=begin元素 则把begin元素放在end位置 end-- 此时调换比较方向……直到begin=end 则数据比较完毕 将轴点元素插入begin或end位置
  3. 为啥要备份:第一次用array[end]元素和轴点元素进行比较时 若end小 则把end元素放在begin位置 begin++ 会覆盖轴点元素
  4. 为啥最初从end元素开始比较:因为begin元素就是轴点元素 难道要轴点元素和轴点元素进行比较?
  5. 注意元素等于轴点元素时 要对元素进行处理 把元素放在自己的对立面 也就是说 end元素=轴点元素 end放在begin的位置 begin元素=轴点元素 begin放在end的位置 为什么要这么做:假设数组array={6,6,6,6,6,6} 随机轴点元素肯定是6 这个6和索引为0的6交换位置 array={6a,6b,6c,6d,6e,6f} 注:abcdef表示不同的6 第一个6是轴点元素 然后进行排序 假设相等元素不操作 直接让begin++或end-- 那么排序后的数组依旧是array={6a,6b,6c,6d,6e,6f} 分割出来的子序列极度不均匀 整个数组没有进行任何操作 6a依旧是轴点元素 排序和不排序没有任何区别 浪费资源 根据快速排序的规定: 对子序列进行上述1 2步骤 直到不能再分割(子序列中只剩下1个元素) 显然刚才的排序并没有达到快速排序的规定 还会继续排序 继续排序也是原来的数组 没有意义 程序会一直执行 所以对于相等的元素 要将其放在自己的对立面 才能将序列分割成2个均匀的子序列

代码如下:

JAVA:

public class QuickSort<E extends Comparable<E>> extends sort.cmp.Sort<E> {
    @Override
    protected void sort() {
        sort(0,array.length);
    }

    private void sort(int begin, int end) {//对[begin,end)范围内的元素进行快速排序
        if (end - begin < 2) return;
        //确定轴点位置
        int mid = pivotIndex(begin,end);
        //对子序列进行快速排序
        sort(begin,mid);
        sort(mid + 1,end);
    }

    private int pivotIndex(int begin, int end){
    	//构造出[begin,end)范围的轴点元素 返回值是轴点元素的最终位置
        //随机选择一个元素和begin位置的元素交换
        swap(begin, begin + (int) (Math.random() * (end - begin)));
        //备份begin位置的元素
        E pivot = array[begin];
        //end指向最后一个元素
        end--;
        while (begin < end){
            while (begin < end){
                if (cmp(pivot,array[end]) < 0){//右边元素>轴点元素
                    end--;
                }else {//右边元素<=轴点元素
                    array[begin++] = array[end];
                    break;
                }
            }

            while (begin < end){
                if (cmp(pivot,array[begin]) > 0){//左边元素<轴点元素
                    begin++;
                }else {//左边元素>=轴点元素
                    array[end--] = array[begin];
                    break;
                }
            }
        }
        //将轴点元素放入最终位置
        array[begin] = pivot;
        //返回轴点元素的位置
        return begin;
    }
}

归并排序

不断将当前序列平均分割成2个子序列 直到不能再分割(序列中只剩1个元素)
不断将两个子序列合并成一个有序序列 直到最终只剩下一个有序序列

实现

  1. 令li是左边数组的第一个索引 le是左边数组的最后一个索引 ri是左边数组的第一个索引 re是左边数组的最后一个索引 ai是合并数组的最后一个元素的索引+1
  2. 我们对array数组进行归并排序 先将array数组的元素分为一个一个 然后再对这一个个局部数组进行合并 显然 我们合并后的数组依旧是array数组 注意:当我们把右边数组的元素放在array[0]的位置时 会破坏左边数组 所以我们必须先备份左边数组leftArray 然后取出左边数组的第一个元素和右边数组的第一个元素进行比较 若右边小于左边 则将右边的元素放在array数组中索引为ai的位置 然后ri++ ai++ 然后取出左边数组的第一个元素和右边数组的第二个元素进行比较 若右边大于左边 则将左边的元素放在array数组中索引为ai的位置 然后li++ ai++ 如此循环执行 直到两数组中某一方为空
  3. 若左边为空 则结束 因为两个数组肯定分别都是排好序的 我是把两个排好序的数组变为一个排好序的数组 若右边为空 则把左边剩余的元素依次移动到array中
 */
public class MergeSort<E extends Comparable<E>> extends sort.cmp.Sort<E> {
    private E[] leftArray;
    @Override
    protected void sort() {
        leftArray = (E[])new Comparable[array.length >> 1];
        sort(0,array.length);
    }

    /*
    用递归分割 把一个数组分为两个数组 再把这两个数组分别分割为两个数组 得到四个数组……
    显然 分割的本质就是把一个数组一分为二 所以可以用递归
     */
    private void sort(int begin, int end) {//对[begin,end)范围的数据进行归并排序
    	//end - begin为数组长度 数组长度小于2说明数组中只有一个或0个元素
        if (end - begin < 2) return;
        int mid = (begin + end) >> 1;
        sort(begin,mid);
        sort(mid,end);
        merge(begin,mid,end);
    }
    private void merge(int begin,int mid,int end) {//将[begin,mid)和[mid,end)范围的序列合并成一个有序序列
        int li = 0,le = mid - begin;
        int ri = mid,re = end;
        int ai = begin;
        for (int i = li; i < le; i++) {//备份左边数组
            leftArray[i] = array[begin + i];
        }
        while (li < le){//如果左边还没结束
            if (ri < re && cmp(array[ri],leftArray[li]) < 0){
                array[ai++] = array[ri++];
            }else {//右边为空 则把左边剩余的元素依次移动到array中
                array[ai++] = leftArray[li++];
            }
        }
    }
}

插入排序

类似于打扑克时所用的排序

实现

  1. 在执行过程中 插入排序将数组分为两部分:头部是已经排好序的 尾部是待排序的
  2. 从头开始扫描每一个元素 每当扫描到一个元素 就将他插入到头部合适的位置(利用二分查找实现) 使得头部数据依旧保持有序 执行过程和打扑克摸牌差不多 最开始你手上有一张牌index[0] 这张牌可看做头部 剩下的牌看做尾部 你摸了一张牌index[1] 和手里的牌比较 然后放在合适的位置 现在你手里的两张牌相当于头部 你又摸牌index[2] 和index[1]比较 若index[1]大 那么index[2]和index[1]交换位置 然后index[2]和index[0]比较 若index[2]大 则不交换继续摸牌
function InsertionSort() {
    for (let begin = 1; begin < array.length; begin++) {
        insert(begin,search(begin));
    }
}

function insert(source, dest) {//将source位置的元素插入到dest位置
    let v = array[source];
    for (let i = source; i > dest; i--) {
        array[i] = array[i - 1];
    }
    array[dest] = v;
}
function search(index){
    let begin = 0;
    let end = index;
    while (begin < end){
        let mid = (begin + end) >> 1;
        if (array[index] < array[mid]){
            end = mid;
        }else {
            begin = mid + 1;
        }
    }
    return begin;//begin = end
}
let array = [9,8,7,6,5,4,3,2,1];
InsertionSort();
console.log(array);

215. 数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

解:方法一(快排):选定轴点元素 进行快排一次 就可以得到轴点元素的位置left 在升序数组中 第K大的元素位于n-k索引的位置 所以比较left与n-k 若left<n-k 第K大的元素一定在轴点元素的右边 去右边找 对右边数组进行快排(选定轴点元素 比较……)若left>n-k 第K大的元素一定在轴点元素的左边 去左边找 对左边数组进行快排(选定轴点元素 比较……)若left=n-k 找到第K大元素 返回
代码如下:

var findKthLargest = function(nums, k) {
    let start = 0;
    let n = nums.length;
    let end = n - 1;
    function quick (left, right) {
        let num = nums[left];
        while (left < right) {
            while (left < right) {
                if (num < nums[right]) {
                    right--;
                }else {
                    nums[left++] = nums[right];
                    break;
                }
            }

            while (left < right) {
                if (num > nums[left]) {
                    left++;
                }else {
                    nums[right--] = nums[left];
                    break;
                }
            }
        }
        nums[left] = num;
        return left;
    }

    let index = quick(start, end);
    while (index !== n - k) {
        if (index < n - k) {
            start = index + 1;
            index = quick(start, end);
        }else {
            end = index - 1;
            index = quick(start, end);
        }
    }
    return nums[index];
}

剑指 Offer 45. 把数组排成最小的数

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:
输入: [10,2]
输出: “102”
示例 2:
输入: [3,30,34,5,9]
输出: “3033459”

解:设数组nums中任意两数字的字符串为x和y,则规定排序判断规则 为:

若拼接字符串 x + y > y + x,则x “大于” y ; 反之,若 x + y < y + x,则 x “小于” y ;
x “小于” y 代表:排序完成后,数组中 x 应在 y 左边;“大于” 则反之。

根据以上规则,套用任何排序方法对 nums 执行排序即可。
代码如下:
JS内置函数:

var minNumber = function(nums) {
  return nums.sort((a, b) => ('' + a + b) - ('' + b + a)).join('');
}

JS快排:

function minNumber(nums) {
	quickSort(nums, 0, nums.length - 1)
	console.log()
	return nums.join('')
}
// ! 该题其实考查的就是排序 但是对比大小的条件变了  数字的话根据大小来比
// ! 把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个  满足这种对比是根据 a+b 和 b+a的比较 决定排序位置
function quickSort(nums, l, r) {
	if (l >= r) return
	let i = l,
		j = r
	tmp = nums[i]
	const pivot = l
	while (i < j) {
		while ('' + nums[j] + nums[pivot] - ('' + nums[pivot] + nums[j]) >= 0 && i < j) j--
		while ('' + nums[i] + nums[pivot] - ('' + nums[pivot] + nums[i]) <= 0 && i < j) i++
		tmp = nums[i]
		nums[i] = nums[j]
		nums[j] = tmp
	}
	nums[i] = nums[pivot]
	nums[pivot] = tmp
	quickSort(nums, l, i - 1)
	quickSort(nums, i + 1, r)
}

剑指 Offer 51. 数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:
输入: [7,5,6,4]
输出: 5

解:「归并排序」与「逆序对」是息息相关的。归并排序体现了 “分而治之” 的算法思想,具体为:
分: 不断将数组从中点位置划分开(即二分法),将整个数组的排序问题转化为子数组的排序问题;
治: 划分到子数组长度为 1 时,开始向上合并,不断将 较短排序数组 合并为 较长排序数组,直至合并至原数组时完成排序;
合并阶段 本质上是 合并两个排序数组 的过程,而每当遇到 左子数组当前元素 > 右子数组当前元素 时,意味着 「左子数组当前元素 至 末尾元素」 与 「右子数组当前元素」 构成了若干 「逆序对」 。
本题利用归并排序 改写归并排序 在并的时候统计逆序对 下图演示其中某一部分过程:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1710. 卡车上的最大单元数

请你将一些箱子装在 一辆卡车 上。给你一个二维数组 boxTypes ,其中 boxTypes[i] = [numberOfBoxesi, numberOfUnitsPerBoxi] :
numberOfBoxesi 是类型 i 的箱子的数量。
numberOfUnitsPerBoxi 是类型 i 每个箱子可以装载的单元数量。
整数 truckSize 表示卡车上可以装载 箱子 的 最大数量 。只要箱子数量不超过 truckSize ,你就可以选择任意箱子装到卡车上。

返回卡车可以装载 单元 的 最大 总数。

示例 1:
输入:boxTypes = [[1,3],[2,2],[3,1]], truckSize = 4
输出:8
解释:箱子的情况如下:
1 个第一类的箱子,里面含 3 个单元。
2 个第二类的箱子,每个里面含 2 个单元。
3 个第三类的箱子,每个里面含 1 个单元。
可以选择第一类和第二类的所有箱子,以及第三类的一个箱子。
单元总数 = (1 * 3) + (2 * 2) + (1 * 1) = 8
示例 2:
输入:boxTypes = [[5,10],[2,5],[4,7],[3,9]], truckSize = 10
输出:91

解:将二维数组按照第二维降序排序:Arrays.sort(boxTypes,(a,b)->b[1]-a[1]);,然后遍历二维数组,如果第一维小于truckSize,则装载该类型的盒子,且truckSize减少数量,如果第一维大于truckSize,则取truckSize,如果装满了(truckSize = 0)就直接返回,在for循环中判断是否装满了

public static int maximumUnits(int[][] boxTypes, int truckSize) {
	int ans = 0;
    // 二维数组按照第二维降序排序 [[5,10],[2,5],[4,7],[3,9]]
    Arrays.sort(boxTypes,(a,b)->b[1]-a[1]);
    for (int i = 0; i < boxTypes.length && truckSize != 0; i++) {
    	if (boxTypes[i][0] <= truckSize) {
        	truckSize -= boxTypes[i][0];
            ans += boxTypes[i][0] * boxTypes[i][1];
		}else {// boxTypes[i][0] > truckSize
        	ans += truckSize * boxTypes[i][1];
            truckSize = 0;
        }
	}
    return ans;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值