左神算法基础班Class-02

荷兰国旗问题

给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)

package basic_algorithm.class_01;

import java.util.Arrays;

public class NetherlandsFlag {

    public static void netherlandsFlag(int[] arr, int l, int r, int num) {
        int min = l - 1;
        int max = r + 1;
        int cur = l;
        while (cur < max) {
            if (arr[cur] < num) {
                swap(arr,++min,cur++);
            }else if(arr[cur]>num){
                swap(arr,--max,cur);
            }else{
                cur++;
            }
        }
    }

    //交换
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        int[] arr={1,4,3,5,2,7,6,5,9,8,0};
        int num=5;
        netherlandsFlag(arr,0,arr.length-1,num);
        System.out.println(Arrays.toString(arr));
    }
}

经典快速排序

在荷兰国旗问题的代码上进行改进 将数组最后一个数作为标准划分,然后再把这个数放到正确的位置。经典快排存在的问题在于:当数据状况很差时,如(1,2,3,4,5,6,7)以7为标准划分,最后只有小于7和等于7的部分,再按6划分…这时每次都只确定了一个数(最后一个数)的位置,时间复杂度为O(N^2)。当数据状况好时,如相等的位置刚好在正中间,那时间复杂度可以通过master公式计算得O(N*logN)。

//快速排序
    public static void quickSort(int[] arr) {
        if (arr == null && arr.length < 2) return;
        quickSort(arr, 0, arr.length - 1);
    }

    public static void quickSort(int[] arr, int l, int r) {
        if (l < r) {
            int[] p = partition(arr, l, r);
            quickSort(arr, l, p[0] - 1);
            quickSort(arr, p[1] + 1, r);
        }
    }
    public static int[] partition(int[] arr, int l, int r) {
        int min = l - 1;
        int max = r;//跟荷兰国旗问题不同 这里是将数组的最后一个元素作为标准进行划分 而不用自己定义
        while (l< max) {
            if (arr[l] < arr[r]) {
                swap(arr, ++min, l++);
            } else if (arr[l] > arr[r]) {
                swap(arr, --max, l);
            } else {
                l++;
            }
        }
        swap(arr, r, max);//把arr[r]放到正确位置
        return new int[]{min + 1, max};//返回相等部分的边界
    }

    //交换
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

随机快速排序(最常用)

随机的选择一个数与最后一个数交换,并将这个随机选择的数作为划分标准进行经典快速排序。这时数据状况很差的概率会大大降低(因为你随机选择一个数是个概率事件 不一定每次都选到状况差的情况)时间复杂度也会是个概率情况,是一个长期期望的复杂度(数学证明出来的 记住就好)。额外空间来自在进行每一次划分时要记录划分的那个位置(断点),否则无法知道下一步对哪个区域操作,进行多少次二分就有多少个断点,所以额外空间复杂度也是个概率情况,在最差情况时是O(N),长期期望是O(logN)。
关键代码: 在经典快排基础上加个随机数与末尾数交换即可
随机快排:时间复杂度O(N*logN),额外空间复杂度O(logN)。

public static void quickSort(int[] arr, int l, int r) {
        if (l < r) {
            swap(arr,l+(int)(Math.random()*(r-l+1)),r);//随机快排  即 随机的选择一个数与末位数交换并将其作为标准进行划分
            int[] p = partition(arr, l, r);
            quickSort(arr, l, p[0] - 1);
            quickSort(arr, p[1] + 1, r);
        }
    }

tips:有时设计算法时,如果想绕开样本原本的数据状况,业界最常用的技巧就是 随机 和 哈希,即打乱原本数据状况。

堆排序(重要!!!)

堆就是一个完全二叉树:(可以用数组表示)
大根堆 – 完全二叉树中任何一个子树的头部都是最大值
小根堆 – 完全二叉树中任何一个子树的头部都是最小值
堆结构非常重要
1, 堆结构的heapInsert与heapify

//该函数实现加入一个新节点并往上调整直到形成大根堆的过程
    private static void heapInsert(int[] arr, int index) {
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }
/**
     * 功能:如果当前0~heapSize已经是大根堆 当index位置的值(变小则下沉)发生变化时 重新调该数组为大根堆
     *
     * @param arr      原始数组(以数组形式实现大根堆)
     * @param index    该位置的值发生变化
     * @param heapSize 0~heapSize为大根堆
     */
    public static void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1;
        while (left < heapSize) {//说明index的左孩子没越界
            //返回左右孩子中较大数的下标 left+1即为右孩子
            int maxInx = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            //返回孩子节点与父节点中较大数的下标
            maxInx = arr[maxInx] > arr[index] ? maxInx : index;
            if (maxInx == index) {
                break;
            }
            //交换
            swap(arr, maxInx, index);
            index = maxInx;
            left = index * 2 + 1;

        }
    }

2, 堆结构的增大和减少 (添加或删去节点都要注意重新调整堆结构)
3, 如果只是建立堆的过程, 时间复杂度为O(N)
4, 优先级队列结构, 就是堆结构
堆排序:时间复杂度O(N*logN),额外空间复杂度O(logN)

public static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) return;
        for (int i = 0; i < arr.length; i++) {
            //0~i上建立大根堆,建立的过程复杂度为O(N)
            heapInsert(arr, i);
        }
        //实现堆排序:将当前堆顶元素与最后一个位置的元素交换 然后缩小堆的范围
        // 即把堆顶(最大数)划分了出去  然后再将去掉最大数后的结构调成大根堆 重复划出去最大数
        int heapSize = arr.length;
        swap(arr, 0, --heapSize);
        while (heapSize > 0) {
            heapify(arr, 0, heapSize);
            swap(arr, 0, --heapSize);
        }
    }

排序算法的稳定性及其汇总

稳定性的含义:经排序算法后,相同的值在原始序列中的相对次序是不变。
稳定性的意义:现实世界中存在需要保持原始数据信息的情况。

  • 平均时间复杂度为O(N^2)

    • 冒泡排序:可以实现成稳定的算法,即遇到相同的值不交换
    • 插入排序:可以实现成稳定的算法,即遇到相同的值就不往前放了
    • 选择排序:不稳定
  • 平均时间复杂度为O(N*logN)

    • 归并排序:可以实现成稳定的算法,即左右两边遇到相同值时保证先copy左边
    • 快速排序:不稳定
    • 堆排序:不稳定

工程中的综合排序算法

  • 当样本量比较小时(小于60) – 直接用插排(因为常数项低)
  • 当数组中存储的是基础数据类型(int,char,double…)的数据 – 用快排(因为基础数据类型无需区分原始顺序 都一样)
  • 当数组中存储的是自己定义的class – 用归并(因为可能需要保存原始信息)

有关排序问题的补充

  • 归并排序的额外空间复杂度可以变成O(1),但是非常难,不 需要掌握,可以搜“归并排序 内部缓存法”
  • 快速排序可以做到稳定性问题,但是非常难,不需要掌握, 可以搜“01 stable sort”
  • 有一道题目,是奇数放在数组左边,偶数放在数组右边,还 要求原始的相对次序不变,碰到这个问题,可以怼面试官。面试官非良人。

比较器

基于比较的排序最重要!!!

package basic_algorithm.class_01;

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;

public class MyComparator<S> {

    public static class Student {
        public String name;
        public int id;
        public int age;

        public Student(String name, int id, int age) {
            this.name = name;
            this.id = id;
            this.age = age;
        }
    }

    //按id升序
    public static class IdAscendingComparator implements Comparator<Student> {

        @Override
        public int compare(Student o1, Student o2) {
            return o1.id - o2.id;
        }

    }

    //降id降序
    public static class IdDescendingComparator implements Comparator<Student> {

        @Override
        public int compare(Student o1, Student o2) {
            return o2.id - o1.id;
        }

    }

    //按age升序
    public static class AgeAscendingComparator implements Comparator<Student> {

        @Override
        public int compare(Student o1, Student o2) {
            return o1.age - o2.age;
        }

    }

    //按age降序
    public static class AgeDescendingComparator implements Comparator<Student> {

        @Override
        public int compare(Student o1, Student o2) {
            return o2.age - o1.age;
        }

    }

    public static void printStudents(Student[] students) {
        for (Student student : students) {
            System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age);
        }
        System.out.println("===========================");
    }

    public static void main(String[] args) {
        Student student1 = new Student("A", 1, 23);
        Student student2 = new Student("B", 2, 21);
        Student student3 = new Student("C", 3, 22);

        Student[] students = new Student[]{student3, student2, student1};
        printStudents(students);

        Arrays.sort(students, new IdAscendingComparator());
        printStudents(students);

        Arrays.sort(students, new IdDescendingComparator());
        printStudents(students);

        Arrays.sort(students, new AgeAscendingComparator());
        printStudents(students);

        Arrays.sort(students, new AgeDescendingComparator());
        printStudents(students);


        //优先级队列就是堆
        PriorityQueue<Student> heap = new PriorityQueue<>(new IdAscendingComparator());
        heap.add(student1);
        heap.add(student2);
        heap.add(student3);
        while (!heap.isEmpty()) {
            Student student = heap.poll();
            System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age);
        }
    }

}

桶排序、计数排序、基数排序

  • 非基于比较的排序,与被排序的样本的实际数据状况很有关系,所以实际中并不经常使用。
  • 时间复杂度O(N),额外空间复杂度O(N)
  • 稳定的排序

桶排序是一个大的逻辑概念,不是具体的排序:

  • 桶相当于按照数据状况设计的容器,然后把元素依次放进对应的桶中。
  • 针对具体的实现桶排序又可以分为计数排序和基数排序。
  • 计数排序和基数排序都是桶排序的具体体现。
  • 基数排序用的桶比较少 一般是十个。

补充问题:给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度O(N),且要求不能用非基于比较的排序。
思路分析:桶排序的思想 数组长度为n 准备n+1个桶 第0个桶存数组的最小值 第n个桶存数组的最大值 然后将数组中的其他数放到合适的桶中 因为共n个数据 n+1个桶 所以第1~n-1这些桶中一定存在一个空桶 我们分析可知 最大差值一定不会存在于同一个桶中 所以我们遍历这些桶 找到当前非空桶的前一个(左边距离最近)的非空桶 用当前桶中的最小值减去找到的这个桶中的最大值 即为最大差值
代码实现:

package basic_algorithm.class_01;

        import java.util.Arrays;

public class MaxGap {
    public static int maxGap(int[] nums) {
        if (nums == null || nums.length < 2) return 0;
        int len = nums.length;
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < nums.length; i++) {
            min = Math.min(min, nums[i]);
            max = Math.max(max, nums[i]);
        }
        if (min == max) return 0;
        boolean[] hasNum = new boolean[len + 1];
        int[] mins = new int[len + 1];
        int[] maxs = new int[len + 1];
        int bid = 0;//桶的下标
        for (int i = 0; i < len; i++) {
            bid = bucket(nums[i], len, min, max);
            mins[bid] = hasNum[bid] ? Math.min(mins[bid], nums[i]) : nums[i];
            maxs[bid] = hasNum[bid] ? Math.max(maxs[bid], nums[i]) : nums[i];
            hasNum[bid] = true;
        }
        int res = 0;
        int lastMax = maxs[0];
        for (int i = 1; i <= len; i++) {
            if (hasNum[i]) {
                //找每一个非空桶和距它最近的左边的非空桶,用当前的最小减前一个最大,即为最大差值
                res = Math.max(res, mins[i] - lastMax);
                lastMax = maxs[i];
            }
        }
        return res;
    }

    //把元素放入合适的桶
    private static int bucket(long num, long len, long min, long max) {
        return (int) ((num - min) * len / (max - min));
    }

    public static void main(String[] args) {
        int[] nums={12,23,10,9,34,56,11};
        Arrays.sort(nums);
        System.out.println(Arrays.toString(nums));//[9, 10, 11, 12, 23, 34, 56]
        System.out.println(maxGap(nums));//22
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值