文章目录
💥带星号的题相对比较难想到思路
select s_id, s_name from Student where s_id in
(
select s_id
from Score
group by s_id
having count(c_id) < (select count(c_id) from Course)
)
基本排序
推荐阅读:《算法笔记》–胡凡
给定待排序数组如下
static int[] arr = {12, 2, 7, 8, 9, 7, 12, 89, 0, 12};
冒泡
通过两数的不断交换,把最大的数一直往后移
@Test
public void bubbleSort() {
int len = arr.length - 1;
//控制长度,交换一次后,最后一个元素就有序了,所以遍历长度减一
for (int l = len; l > 0; l--) {
//两数交换
for (int i = 0; i < l; i++) {
if (arr[i] > arr[i + 1]) {
int tmp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = tmp;
}
}
}
System.out.println(Arrays.toString(arr));
}
⚡️优化,对于有序序列,上面的代码需要将两个循环都跑一遍,但其实只需要执行一次内循环就够了,也就是当内循环没有一个元素发生交换,出来外循环就可以break了。从而也可得出冒泡的最好的时间复杂度是O(N)。
💡冒泡是稳定算法,也就是相同元素在排序的过程中仍然能够保证相对位置不变。前提是if判断条件中,等于时不要进行交换(也没必要交换😅)
插入
跟前面的元素做比较,大的则往后移动数组,否则找到插入位置。总是保证前面的序列有序
@Test
public void insertSort() {
int len = arr.length;
//从第二个元素开始
for (int i = 1; i < len; i++) {
int tmp = arr[i];
int j = i;
//移动
while (j > 0 && arr[j - 1] > tmp) {
arr[j] = arr[j - 1];
j--;
}
//插入
arr[j] = tmp;
}
System.out.println(Arrays.toString(arr));
}
💡插入排序也是稳定排序,前提也是元素相等时不要后移😅,最好的时间复杂度也是O(N),也就是有序的时候
选择
每次选择最小的数,换到最前面。
@Test
public void chooseSort() {
int len = arr.length - 1;
for (int i = 0; i < len; i++) {
//求最小值
int min = i;
for (int j = i + 1; j <= len; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
//交换
int tmp = arr[min];
arr[min] = arr[i];
arr[i] = tmp;
}
System.out.println(Arrays.toString(arr));
}
💡选择排序最好时间复杂度也是O(N2),因为你每次都需要选最小值,即便有序你也要选。而且它是不稳定的算法,举个211例子,哦不对,是221例子(211也不过如此😅),第一个2会和1交换导致,两个2的相对顺序被打乱。
快排
双指针(N) * 递归(logN)
1.产生主元,比主元大的放到右边,小的放到左边
2.对左右两边进行递归,重复1
总是以左边的元素作为主元,但需要在数组中随机找一个元素进行替换,否则当数组有序时,主元没有办法将数组划分为两个长度接近的区间,也就是一大一小,此时复杂度就会变为O(N),而不是logN,所以快排的最坏时间复杂度是O(N2)
//用于产生随机位置
private Random random = new Random();
@Test
public void quickSort() {
dfs_qs(0, arr.length - 1);
System.out.println(Arrays.toString(arr));
Arrays.sort(arr);
}
//递归
private void dfs_qs(int left, int right) {
//单个元素,无需继续排序
if (left < right) {
//主元位置
int mid = findMiddle(left, right);
//左右区间递归,继续排序--注意此时主元就不用排序了
dfs_qs(left, mid - 1);
dfs_qs(mid + 1, right);
}
}
//找主元
private int findMiddle(int left, int right) {
//随机产生主元,并交换到前面
int rd = random.nextInt(right - left + 1) + left;
swap(rd, left);
//保存主元
int tmp = arr[left];
// 双指针移动:比主元大的放到右边,小的放到左边
while (left < right) {
while (left < right && arr[right] >= tmp) right--;
arr[left] = arr[right]; //小的移到最左边,空出左边的位置
while (left < right && arr[left] <= tmp) left++;
arr[right] = arr[left]; //大的移到右边,空出右边的位置
}
//更新主元位置,此时主元有序
arr[left] = tmp;
return left;
}
private void swap(int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
💡快排是不稳定算法,从主元获取就可以看出,它是一种随机的,这本就无法保证元素的相对顺序。
归并
不断二分递归,直到只有一个数,然后在不断返回合并。
/**
* 二路归并
*/
@Test
public void mergeSort() {
dfs_ms(0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
private void dfs_ms(int left, int right) {
if (left < right) {
int mid = (right - left) / 2 + left;
// 由于mid的计算公式会偏向左边,所以不能是mid-1 mid,否则会StackOverflowError
//因为两个的数的计算,下标一直都是第一个数,如果按mid-1和mid就会进行不断的递归,直到栈溢出
dfs_ms(left, mid);
dfs_ms(mid + 1, right);
//合并
mergeArray(left, mid, right);
}
}
private void mergeArray(int left, int mid, int right) {
int t = 0;
int[] tmp = new int[right - left + 1];
int i = left;
int j = mid + 1;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
tmp[t++] = arr[i++];
} else {
tmp[t++] = arr[j++];
}
}
//将剩余的元素加入临时数组
while (i <= mid) tmp[t++] = arr[i++];
while (j <= right) tmp[t++] = arr[j++];
//f的作用就是保证left不被修改
System.arraycopy(tmp, 0, arr, left, tmp.length);
}
⚡️归并是稳定算法,也需要前提,也就是合并时,左右两边如果相等if (arr[f] <= arr[s])
,先获取左边的。不过归并需额外的空间总和为O(N)–tmp数组
归并排序练习
小和问题
反向思路:从左往右,有多少个比当前元素a大的数,就产生多少个a的小和。
以1, 3, 5, 2, 4, 6为例,比1大的有5个元素,所以产生5个1小和,比3大的有3个,所以产生3个3的小和,即9,以此类推,5产生5,2产生4,4产生4,6产生0,所以数组小和为5+9+5+4+4+0=27
具体就是在归并排序时,if (arr[f] <= arr[s])
时进行累加即可。
类似题目:剑指 Offer 51. 数组中的逆序对
进阶排序
jdk排序函数
# 数组排序
Arrays.sort();
# 集合排序
Collections.sort();
集合排序
指的是TreeSet等排好序的集合
414. 第三大的数
找数组中第三大的数,其中相同元素的排位相同
思路一:类比求数组最大值,数组第二大值。具体就是更新最大值时,先将第二大值赋给第三大值,最大值赋给第二大值,然后在更新;更新第二大值时,先将第二大值赋给第三大值,然后在更新,以此类推。
注意:三个值不能相等,相等代表不存在。例如[2,2,2]仅有最大值。
public int thirdMax(int[] nums) {
//使用Long
long max = Long.MIN_VALUE;
long second = Long.MIN_VALUE;
long third = Long.MIN_VALUE;
//遍历
for(int num : nums) {
if(max < num) {
third = second;
second = max;
max = num;
} else if(second < num && num < max) {//注意:num<max 保证第一大!=第二大
third = second;
second = num;
} else if(third < num && num < second) {//同上
third = num;
}
}
//如果不存在第三大则返回最大值。
return third != Long.MIN_VALUE ? (int)third : (int)max;
}
思路二:集合排序
用TreeSet存放,TreeSet是有序不重复集合,避免了重复问题,实现思路是添加元素时,仅保存3个,也就是当集合size大于3就移除第一个元素(这样每次仅保留最大的三个值),而最后留下的3个元素为前三大值。
public int thirdMax(int[] nums) {
TreeSet<Integer> set = new TreeSet<>();
for(int num: nums) {
set.add(num);
if(set.size() > 3) {
set.remove(set.first());
}
}
return set.size() ==3 ? set.first() : set.last();
}
堆排序
215. 数组中的第K个最大元素
跟上一道类似,除了变为动态的找第k个大数,同时要求找数组中第三大的数,其中相同元素的排位不同。也就是44算两个排位
思路一:快排,无话可说。。。
public int findKthLargest(int[] nums, int k) {
Arrays.sort(nums);
return nums[nums.length - k];
}
思路二:堆排序,如果不了解堆的话,建议学下堆的知识
学习堆
//注意数组下标从0开始
public int findKthLargest(int[] nums, int k) {
//建堆
createHeap(nums);
//堆排序
int l = nums.length - 1;
for (int i = l; i >= 0; i--) {
//交换首节点和尾节点,即把最大值放到最后
swap(nums, 0, i);
//调整首节点
downJust(nums, 0, i);
}
return nums[nums.length - k];
}
public void createHeap(int[] arr) {
//仅需调整有子节点的节点,即n/2,完全二叉树的特点
int mid = arr.length / 2;
for (int i = mid; i >= 0; i--) {
downJust(arr, i, arr.length);//往下调整
}
}
public void downJust(int[] arr, int i, int l) {
int j = 2 * i + 1;
while (j < l) {
//左右两节点先比较,找出最大
if ((j + 1) < l && arr[j + 1] > arr[j]) j++;
if (arr[j] > arr[i]) {
//交换
swap(arr, i, j);
//继续往下调整
i = j;
j = 2 * i + 1;
} else {
break;
}
}
}
public void swap(int[] arr, int i, int j) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
思路三:PriorityQueue—小顶堆,添加元素的时候,仅保留k个即可(小的会被移除),这样的话,最后的堆顶即为第k大的数。思路类似上一题使用TreeSet集合。
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> smallHeap = new PriorityQueue<>();
int count = 0;
for(int num : nums) {
count++;
smallHeap.add(num);
if(count > k) smallHeap.poll();
}
return smallHeap.peek();
}
堆练手:面试题 17.14. 最小K个数
347. 前 K 个高频元素
思路:哈希表+堆排序
首先用哈希表统计每个整数出现的次数。然后用堆排序,直接使用小顶堆,不过这里需要定制排序,我们要排序的是key-value,并以value作为排序准则,由于都是int类型,可简单用数组(int[])表示,即arr[0]=key,arr[1]=value(比较难想到)。也可以使用一个类,存放这两个变量。(比较通用,具体看下一题)
class Solution {
public int[] topKFrequent(int[] nums, int k) {
//哈希计数
Map<Integer,Integer> map = new HashMap<>();
for (int tmp : nums) {
map.put(tmp, map.getOrDefault(tmp,0) + 1);
}
//使用小顶堆进行排序(不能用TreeSet,因为频率可以重复)
PriorityQueue<int[]> heap = new PriorityQueue<>(new Comparator<int[]>(){
@Override
public int compare(int[] o1, int[] o2){
return Integer.compare(o2[1],o1[1]);//逆序,按频率排序
}
});
for(Map.Entry<Integer,Integer> entry :map.entrySet()) {
int key = entry.getKey();
int value = entry.getValue();
heap.add(new int[]{key,value});
}
//结果遍历
int[] res = new int[k];
int i = 0;
while(i < k) {
res[i] = heap.poll()[0];
i++;
}
return res;
}
}
*剑指 Offer 41. 数据流中的中位数
思路一:想不到只能,直接暴力快排
链接:快排
思路二:大顶堆+小顶堆
大顶堆存放小于等于中位数的数据
小顶堆存放大于等于中位数的数据
也就是将数据划分为两部分,中位数就位于两堆的第一个元素。
class MedianFinder {
//大顶堆
PriorityQueue<Integer> big;
//小顶堆
PriorityQueue<Integer> small;
public MedianFinder() {
big = new PriorityQueue<>(new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o2, o1);
}
});
//默认小顶堆
small = new PriorityQueue<>();
}
public void addNum(int num) {
if(big.size() <= small.size()) {
if(small.isEmpty()) {
//首次添加
big.add(num);
} else {
//添加的元素需要小于等于small中的最小值
if(num <= small.peek()) {
big.add(num);
} else {
//跟small的最小值交换
big.add(small.poll());
small.add(num);
}
}
} else {
//添加的元素需要大于等于big中的最大值
if(num >= big.peek()) {
small.add(num);
} else {
//跟big的最大值交换
small.add(big.poll());
big.add(num);
}
}
}
public double findMedian() {
int l = big.size() + small.size();
if((l & 1) == 0) {
//偶数,则返回(big的最小值+small的最大值)/2.0
return (big.peek() + small.peek()) / 2.0;
} else {
//奇数,则返回big的最小值
return big.peek();
}
}
}
桶排序
451. 根据字符出现频率排序
思路一:哈希表+堆排序
与上一题步骤一样,首先用哈希表统计每个字符出现的次数。然后用堆排序,使用小顶堆,需要定制排序,我们要排序的同样是key-value,并以value作为排序准则,这里由于类型不一致,通过使用一个KV类,来存放这两个变量。
class Solution {
public String frequencySort(String s) {
//哈希计数
Map<Character,Integer> map = new HashMap<>();
int l = s.length();
while(l-- > 0) {
char tmp = s.charAt(l);
map.put(tmp,map.getOrDefault(tmp,0) + 1);
}
//堆排序
PriorityQueue<KV> queue = new PriorityQueue<>(new Comparator<KV>(){
@Override
public int compare(KV o1, KV o2) {
return Integer.compare(o2.getValue(),o1.getValue());//逆序,根据value排序
}
});
for(Map.Entry<Character,Integer> entry :map.entrySet()) {
queue.add(new KV(entry.getKey(),entry.getValue()));
}
//字符串拼接结果
StringBuilder sb = new StringBuilder();
while(!queue.isEmpty()) {
KV obj = queue.poll();
int len = obj.getValue();
while(len-- > 0) sb.append(obj.getKey());
}
return sb.toString();
}
}
//用于存放k-v类
class KV{
private char key;
private int value;
public KV(char key, int value) {
this.key = key;
this.value = value;
}
public char getKey(){
return key;
}
public int getValue(){
return value;
}
}
思路二:桶排序
以字符出现次数作为数组下标,出现一次的字符放到list[1]中,出现两次的放到list[2]中,以此类推。
class Solution {
public String frequencySort(String s) {
//哈希计数
Map<Character,Integer> map = new HashMap<>();
int l = s.length();
while(l-- > 0) {
char tmp = s.charAt(l);
map.put(tmp,map.getOrDefault(tmp,0) + 1);
}
//桶排序
l = s.length() + 1; //数组最大长度为字符串长度加一,当字符串只有一种字符时,例如aaa
List<Character>[] listArr = new ArrayList[l]; //数组不加<>
for(Map.Entry<Character,Integer> entry: map.entrySet()) {
char key = entry.getKey();
int value = entry.getValue();
if(listArr[value] == null) listArr[value] = new ArrayList<Character>();
listArr[value].add(key);
}
//字符串拼接结果
StringBuilder sb = new StringBuilder();
//数组反向遍历,即可实现有序
for(int i = l - 1; i > 0; i--) {
if(listArr[i] == null) continue;
int size = listArr[i].size(); //次数为i的字符有多少个,例如tree,次数为1的有t和r
for(int j =0; j < size; j++) {
char tmp = (Character)listArr[i].get(j);
int loop = i;
while(loop-- > 0) sb.append(tmp);
}
}
return sb.toString();
}
}
提示: 如果想练习桶排序的话,可自行改造上一题。
总结
冒泡、插入有最好时间、都是稳定算法,选择除了时间复杂度一样,其它都不行。
快排、归并、堆排序时间复杂度一样,各自有优点:快排有最坏时间复杂度,占用空间、不稳定;归并稳定,但占用空间;堆不稳定但不占空间。