排序题目汇总(基础+进阶)


💥带星号的题相对比较难想到思路

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数组

为什么快排和归并是nlogn

归并排序练习
小和问题

反向思路:从左往右,有多少个比当前元素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])时进行累加即可。

B站视频左神,从1:01:25分开始食用。代码

类似题目:剑指 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();
    }
}

提示: 如果想练习桶排序的话,可自行改造上一题。

总结

冒泡、插入有最好时间、都是稳定算法,选择除了时间复杂度一样,其它都不行。
快排、归并、堆排序时间复杂度一样,各自有优点:快排有最坏时间复杂度,占用空间、不稳定;归并稳定,但占用空间;堆不稳定但不占空间。
在这里插入图片描述

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值