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 个元素
Top K问题,具体学习看一下这个https://zhuanlan.zhihu.com/p/114699207
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. 按照字符出现次数对字符串排序
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();
}
}
荷兰国旗问题
荷兰国旗包含三种颜色:红、白、蓝。
有三种颜色的球,算法的目标是将这三种球按颜色顺序正确地排列。它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。
循环不变量:
1、[0,p0)区间内元素全是0
2、 [p0,curr)区间内元素全是1
3、 (p2,len-1]区间内元素全是2
4、[curr,p2]区间元素待遍历
代码中的元素交换、指针增增减减都是为了保证以上循环不变量的性质。
###########################################################################################
对于nums[curr] == 0时为什么curr++的问题,分两种情况讨论即可:
-
curr != p0 则[p0,curr)左闭右开区间的元素全是1,两者交换后nums[curr]一定是1,所以直接curr++
-
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;
}
}