Leedcode算法专题训练(排序)

排序

快速排序

用于求解 Kth Element 问题,也就是第 K 个元素的问题。

可以使用快速排序的 partition() 进行实现。需要先打乱数组,否则最坏情况下时间复杂度为 O(N2)。

堆排序

用于求解 TopK Elements 问题,也就是 K 个最小元素的问题。可以维护一个大小为 K 的最小堆,最小堆中的元素就是最小元素。最小堆需要使用大顶堆来实现,大顶堆表示堆顶元素是堆中最大元素。这是因为我们要得到 k 个最小的元素,因此当遍历到一个新的元素时,需要知道这个新元素是否比堆中最大的元素更小,更小的话就把堆中最大元素去除,并将新元素添加到堆中。所以我们需要很容易得到最大元素并移除最大元素,大顶堆就能很好满足这个要求。

堆也可以用于求解 Kth Element 问题,得到了大小为 k 的最小堆之后,因为使用了大顶堆来实现,因此堆顶元素就是第 k 大的元素。

快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。

可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。

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

快速排序选择:

public class Solution {
    public int findKthLargest(int[] nums, int k) {
        int len = nums.length;
        int left = 0;
        int right = len - 1;

        // 转换一下,第 k 大元素的索引是 len - k
        int target = len - k;

        while (true) {
            int index = partition(nums, left, right);
            if (index == target) {
                return nums[index];
            } else if (index < target) {
                left = index + 1;
            } else {
                right = index - 1;
            }
        }
    }

    public int partition(int[] nums, int left, int right) {
        int pivot = nums[left];
        int j = left;
        for (int i = left + 1; i <= right; i++) {
            if (nums[i] < pivot) {
                // 小于 pivot 的元素都被交换到前面
                j++;
                swap(nums, j, i);
            }
        }
        // 在之前遍历的过程中,满足 [left + 1, j] < pivot,并且 (j, i] >= pivot
        swap(nums, j, left);
        // 交换以后 [left, j - 1] < pivot, nums[j] = pivot, [j + 1, right] >= pivot
        return j;
    }

    private void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
}

快速排序法模板背诵:

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k == 0 || arr.length == 0) {
            return new int[0];
        }
        // 最后一个参数表示我们要找的是下标为k-1的数
        return quickSearch(arr, 0, arr.length - 1, k - 1);
    }
 
    private int[] quickSearch(int[] nums, int lo, int hi, int k) {
        // 每快排切分1次,找到排序后下标为j的元素,如果j恰好等于k就返回j以及j左边所有的数;
        int j = partition(nums, lo, hi);
        if (j == k) {
            return Arrays.copyOf(nums, j + 1);
        }
        // 否则根据下标j与k的大小关系来决定继续切分左段还是右段。
        return j > k? quickSearch(nums, lo, j - 1, k): quickSearch(nums, j + 1, hi, k);
    }
 
    // 快排切分,返回下标j,使得比nums[j]小的数都在j的左边,比nums[j]大的数都在j的右边。
    private int partition(int[] nums, int lo, int hi) {
        int v = nums[lo];
        int i = lo, j = hi + 1;
        while (true) {
            while (++i <= hi && nums[i] < v);
            while (--j >= lo && nums[j] > v);
            if (i >= j) {
                break;
            }
            int t = nums[j];
            nums[j] = nums[i];
            nums[i] = t;
        }
        nums[lo] = nums[j];
        nums[j] = v;
        return j;
    }
}
public int findKthLargest(int[] nums, int k) {
    k = nums.length - k;
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int j = partition(nums, l, h);
        if (j == k) {
            break;
        } else if (j < k) {
            l = j + 1;
        } else {
            h = j - 1;
        }
    }
    return nums[k];
}

private int partition(int[] a, int l, int h) {
    int i = l, j = h + 1;
    while (true) {
        while (a[++i] < a[l] && i < h) ;
        while (a[--j] > a[l] && j > l) ;
        if (i >= j) {
            break;
        }
        swap(a, i, j);
    }
    swap(a, l, j);
    return j;
}

private void swap(int[] a, int i, int j) {
    int t = a[i];
    a[i] = a[j];
    a[j] = t;
}

 

排序 :时间复杂度 O(NlogN),空间复杂度 O(1)

public int findKthLargest(int[] nums, int k) {
    Arrays.sort(nums);
    return nums[nums.length - k];
}

 :时间复杂度 O(NlogK),空间复杂度 O(K)。

(1)min-heap:
PriorityQueue<ListNode> queue = new PriorityQueue<>((x, y) -> x.val - y.val);

(2)max-heap:
PriorityQueue<ListNode> queue = new PriorityQueue<>((x, y) -> y.val - x.val);

A = new PriorityQueue<>(); // 小顶堆,保存较大的一半
B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半
public int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> pq = new PriorityQueue<>(); // 小顶堆
    for (int val : nums) {
        pq.add(val);
        if (pq.size() > k)  // 维护堆的大小为 K
            pq.poll();
    }
    return pq.peek();
}

桶排序

1. 出现频率最多的 k 个元素

347. 前 K 个高频元素

Top K问题,具体学习看一下这个https://zhuanlan.zhihu.com/p/114699207

本文提供 4 种解法,其中 堆 和 二叉搜索树 的时间复杂度是 O(NlogK)O(NlogK),计数排序(桶排序) 和 快排变形 的时间复杂度是 O(N)O(N)。

 

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 统计每个数字出现的次数
        // Map<Integer, Integer> counterMap = IntStream.of(nums).boxed().collect(Collectors.toMap(e -> e, e -> 1, Integer::sum));
        Map<Integer,Integer> counterMap=new HashMap<>();
        for(int num:nums){
            counterMap.put(num,counterMap.getOrDefault(num,0)+1);
        }
        // 一个数字最多出现 nums.length 次,因此定义一个长度为 nums.length + 1 的数组,freqList[i] 中存储出现次数为 i 的所有数字。
        List<Integer>[] freqList = new List[nums.length + 1];
        for (int i = 0; i < freqList.length; i++) {
            freqList[i] = new ArrayList<>();
        }
        counterMap.forEach((num, freq) -> {
            freqList[freq].add(num);
        });
        // 按照出现频次,从大到小遍历频次数组,构造返回结果。
        int[] res = new int[k];
        int idx = 0;
        for (int freq = freqList.length - 1; freq > 0; freq--) {
            for (int num: freqList[freq]) {
                res[idx++] = num;
                if (idx == k) {
                    return res;
                }
            }
        }
        return res;
    }
}

2. 按照字符出现次数对字符串排序

451. 根据字符出现频率排序

class Solution {
    public String frequencySort(String s) {
        HashMap<Character,Integer> str_map=new HashMap<>();
        char[] nums=s.toCharArray();
        for(char c: nums){
            str_map.put(c, str_map.getOrDefault(c,0)+1);
        }
        LinkedList<Character>[] freq_list = new LinkedList[s.length()+1];
        for(int i=0;i<freq_list.length;i++){
            freq_list[i]=new LinkedList<>();
        }
        str_map.forEach((num,freq)->{
            freq_list[freq].add(num);
        });
        StringBuilder str=new StringBuilder();
        for(int i=freq_list.length-1; i>=0; i--){
            for(Character ch:freq_list[i]){
                for(int j=0;j<i;j++){
                   str.append(ch); 
                }  
            }
        }
        return str.toString();

    }
}
class Solution {
    public String frequencySort(String s) {
        Map<Character,Integer> frequencyForNum=new HashMap<>();
        for(char c: s.toCharArray()){
            frequencyForNum.put(c,frequencyForNum.getOrDefault(c,0)+1);
        }
        List<Character>[] frequencyBucket=new ArrayList[s.length()+1];
        for(char c:frequencyForNum.keySet()){
            int f=frequencyForNum.get(c);
            if(frequencyBucket[f]==null){
                frequencyBucket[f]=new ArrayList<>();
            }
            frequencyBucket[f].add(c);
        }
        StringBuilder str=new StringBuilder();
        for(int i=frequencyBucket.length-1;i>=0;i--){
            if(frequencyBucket[i]==null){
                continue;
            }
            for(char c:frequencyBucket[i]){
                for(int j=0;j<i;j++){
                    str.append(c);
                }
            }
        }
        return str.toString();

    }
}

荷兰国旗问题

荷兰国旗包含三种颜色:红、白、蓝。

有三种颜色的球,算法的目标是将这三种球按颜色顺序正确地排列。它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。

75. 颜色分类

循环不变量:

1、[0,p0)区间内元素全是0

2、 [p0,curr)区间内元素全是1

3、 (p2,len-1]区间内元素全是2

4、[curr,p2]区间元素待遍历

代码中的元素交换、指针增增减减都是为了保证以上循环不变量的性质。

###########################################################################################

对于nums[curr] == 0时为什么curr++的问题,分两种情况讨论即可:

  1. curr != p0 则[p0,curr)左闭右开区间的元素全是1,两者交换后nums[curr]一定是1,所以直接curr++

  2. curr == p0,满足循环不变量性质,直接curr++

 

class Solution {
    public void sortColors(int[] nums) {
        int zero = -1, one = 0, two = nums.length;
        while (one < two) {
            if (nums[one] == 0) {
                // 如果是2 0 1 2 的话 左边交换完 nums[cur]的值是1, 我理解是因为左边交换完要么是0,要么是1, 都不用再判断, 因此cur后移, 而右边交换完 可能是2 需要再次判断, 因此cur不用后移
                swap(nums, ++zero, one++);
            } else if (nums[one] == 2) {
                swap(nums, --two, one);
            } else {
                ++one;
            }
        }
    }

    private void swap(int[] nums, int i, int j) {
        int t = nums[i];
        nums[i] = nums[j];
        nums[j] = t;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值