「持续100 天更新 Java 相关面试题」—— 第 5 天

前言

今天是更新第 5 天,今天的主要内容还是 Java 基础 - 「排序」和「二分」

正文

1. 排序

冒泡排序

依次比较两个相邻的元素,如果顺序错误就把他们交换过来,两两都比较完一遍称为一轮冒泡,重复以上步骤直到数组有序。

n 个元素在第一轮冒泡时需要比较 n - 1次,第一轮比较完成后数组最大/最小的元素会在最后的位置,后续每轮冒泡完成后,下一次比较次数总会减一。

如果在某一轮的冒泡完成后,没有发生元素交换,说明数组排序完成,结束循环即可。

// 版本一
int[] nums = {5, 2, 7, 4, 1, 3, 8, 9};

int n = nums.length;
for (int i = 0; i < n - 1; i++) {
    boolean flag = true;
    for (int j = 0; j < n - 1 - i; j++) {
        if (nums[j] > nums[j + 1]) {
            int t = nums[j];
            nums[j] = nums[j + 1];
            nums[j + 1] = t;
            flag = false;
        }
    }
    if (flag) {
        break;
    }
}

System.out.println(Arrays.toString(nums));
// 进一步优化: 记录最后一次发生交换的下标作为下一轮冒泡的比较次数,如果次数为0,说明数组有序
int[] nums = {5, 2, 7, 4, 1, 3, 8, 9};

int n = nums.length;

int cnt = n - 1;
while (true) {
    int last = 0;
    for (int j = 0; j < cnt; j++) {
        if (nums[j] > nums[j + 1]) {
            int t = nums[j];
            nums[j] = nums[j + 1];
            nums[j + 1] = t;
            last = j;
        }
    }
    cnt = last;
    if (cnt == 0) {
        break;
    }
}

System.out.println(Arrays.toString(nums));

选择排序

将数组划分为两个部分,已经完成排序的部分和没有完成排序的部分,每一轮从未排序的部分中找出最小/最大的元素,放入已经排序的部分,重复以上步骤直到数组有序

int[] nums = {5, 2, 7, 4, 1, 3, 8, 9};

int n = nums.length;

for (int i = 0; i < n - 1; i++) {
    int idx = i;
    for (int j = i + 1; j < n; j++) {
        if (nums[j] < nums[idx]) {
            idx = j;
        }
    }
    if (idx != i) {
        int t = nums[i];
        nums[i] = nums[idx];
        nums[idx] = t;
    }
}

System.out.println(Arrays.toString(nums));

选择排序与冒泡排序的比较

  1. 二者平均时间复杂度都是O(n^2)
  2. 选择排序一般要快于冒泡,因为其交换次数少
  3. 如果集合有序度高,冒泡优于选择
  4. 冒泡排序属于稳定排序算法,而选择排序属于不稳定排序算法

插入排序

int[] nums = {5, 2, 7, 4, 1, 3, 8, 9};

int n = nums.length;

// i 为待插入元素的索引
for (int i = 1; i < n; i++) {
    // val 为待插入的元素值
    int val = nums[i];
    // j 表示已排序区域的索引
    int j;
    for (j = i - 1; j >= 0; j--) {
        if (val < nums[j]) {
            nums[j + 1] = nums[j];
        } else {
            break;
        }
    }
    nums[j + 1] = val;
}

System.out.println(Arrays.toString(nums));

插入排序和选择排序比较

  1. 二者平均时间复杂度都是O(n^2)
  2. 大部分情况下,插入略优于选择
  3. 有序集合插入的时间复杂度为O(n)
  4. 插入排序属于稳定排序算法,而选择排序属于不稳定排序

希尔排序

是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的数字越来越多,当增量减至 1 时,整个数组恰被分成一组,算法便终止。

快速排序

基于分治的一种快排方法,平均时间复杂度是O(nlog2n),最坏时间复杂度O(n^2),数据量较大时,优势非常明显,属于不稳定排序

单边循环快排(lomuto洛穆托分区方案)

  • 选择最右元素作为基准点元素
  • j指针负责找到比基准点小的元素,一旦找到则与i进行交换
  • i指针维护小于基准点元素的边界,也是每次交换的目标索引
  • 最后基准点与i交换,i即为分区位置

双边循环快排(并不完全等价于hoare霍尔分区方案)

  • 选择最左元素作为基准点元素

  • j指针负责从右向左找比基准点小的元素

  • i指针负责从左向右找比基准点大的元素

  • 一旦找到二者交换,直至i,j相交

  • 最后基准点与i(此时i与j相等)交换,i即为分区位置

    public static void quickSort(int[] arr, int l, int r) {
        // 分区中只有一个元素时,退出排序
        if (l >= r) return;
    	
        // 基准点为数组的中间元素,比基准点小的元素放在基准点左边,反之放在右边
        int x = arr[l + r >> 1], i = l - 1, j = r + 1;
    	
        // 类似于双边循环快排的实现
        while (i < j) {
            while (arr[++i] < x);
            while (arr[--j] > x);
            if (i < j) {
                int t = arr[i];
                arr[i] = arr[j];
                arr[j] = t;
            }
        }
        // 循环结束后,递归处理左右两个分区
        quickSort(arr, l, j);
        quickSort(arr, j + 1, r);
    }
    

归并排序

基于分治,将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序方法。

public static void mergeSort(int[] arr, int l, int r) {
    if (l >= r) return;
	
    // 以中间位置为分界点
    int mid = l + r >> 1;

    // 先递归分解,再合并
    mergeSort(arr, l, mid);
    mergeSort(arr, mid + 1, r);
	
    int k = 0, i = l, j = mid + 1;

    while (i <= mid && j <= r) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        } else {
            temp[k++] = arr[j++];
        }
    }

    while (i <= mid) temp[k++] = arr[i++];
    while (j <= r) temp[k++] = arr[j++];

    for (i = l, j = 0; i <= r; i++, j++) {
        arr[i] = temp[j];
    }
}

JDK Arrays 类的 sort 方法

Arrays.sort(a);

使用ctrl+左键进入sort()方法

Arrays.sort()

关于sort()的方法一共有14个,就目前调用的来看是以下这种最基础的。

// 默认排序的区间为整个数组
public static void sort(int[] a) {
    DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}

// 指定了排序的开始位置和结束位置
public static void sort(int[] a, int fromIndex, int toIndex) {
    // 进行边界检查
    rangeCheck(a.length, fromIndex, toIndex);
    DualPivotQuicksort.sort(a, fromIndex, toIndex - 1, null, 0, 0);
}
DualPivotQuicksort

DualPivotQuicksort即双轴快排,定义了七种原始类型的排序方法。实现了sort方法并且定义了以下调整参数:

// 构造器私有防止实例化
private DualPivotQuicksort() {}

// 归并排序的最大运行次数
private static final int MAX_RUN_COUNT = 67;

// 归并排序的最大运行长度
private static final int MAX_RUN_LENGTH = 33;

// 如果要排序的数组的长度小于该常数,则优先使用快速排序而不是归并排序
private static final int QUICKSORT_THRESHOLD = 286;

// 如果要排序的数组的长度小于此常数,则优先使用插入排序而不是快速排序
private static final int INSERTION_SORT_THRESHOLD = 47;

// 如果要排序的字节数组的长度大于该常数,则优先使用计数排序而不是插入排序
private static final int COUNTING_SORT_THRESHOLD_FOR_BYTE = 29;

// 如果要排序的 short 或 char 数组的长度大于此常数,则优先使用计数排序而不是快速排序
private static final int COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR = 3200;
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);

该方法定义:

static void sort(int[] a, int left, int right,
                     int[] work, int workBase, int workLen)

进入DualPivotQuicksort的sort方法:

 static void sort(int[] a, int left, int right,
                     int[] work, int workBase, int workLen) {
        // Use Quicksort on small arrays
        if (right - left < QUICKSORT_THRESHOLD) {
            sort(a, left, right, true);
            return;
        }

首先进行了判断,如果要排序的数组小于了之前定义的QUICKSORT_THRESHOLD=286,则优先使用快速排序而不是归并排序,即进入if中的排序sort(a, left, right, true);

DualPivotQuicksort.sort(a, left, right, true)

该方法定义:

private static void sort(int[] a, int left, int right, boolean leftmost)

进入if中的sort(a, left, right, true)方法,我们只截取他的逻辑部分而非排序实现部分。

private static void sort(int[] a, int left, int right, boolean leftmost) {
        int length = right - left + 1;

        // Use insertion sort on tiny arrays
        if (length < INSERTION_SORT_THRESHOLD) {
            if (leftmost) {
                /*
                 * Traditional (without sentinel) insertion sort,
                 * optimized for server VM, is used in case of
                 * the leftmost part.
                 */
                for (int i = left, j = i; i < right; j = ++i) {
                    int ai = a[i + 1];
                    while (ai < a[j]) {
                        a[j + 1] = a[j];
                        if (j-- == left) {
                            break;
                        }
                    }
                    a[j + 1] = ai;
                }
            } else {
                // 其他处理逻辑.....

该方法中,首先判断了数组长度是否小于INSERTION_SORT_THRESHOLD=47,如果小于就使用插入排序,而不是快速排序。leftmost是来选择使用传统的(无标记)插入排序还是成对插入排序,leftmost是表示此部分是否在范围内的最左侧,因为我们最先开始调用的就是基础的sort,没有其他参数,所以就是从头开始排序,leftmost便默认为true,使用传统(无标记)插入排序,如果为false,使用成对插入排序。

总结

JDK Arrays 类的 sort 方法会根据待排序数组的长度决定使用哪种排序方法。

2. 二分查找

这里二分写的比较简单,不了解二分的可以去看一下这个视频 二分查找 或者去找一些文章看一下。

Arrays.binarySearch(int[] a, key)
Arrays.binarySearch(int[] a, int fromIndex, int toIndex, int key)
    
// Arrays类的二分查找方法源码
// 如果不指定开始位置和结束位置,默认查找范围为整个数组
public static int binarySearch(int[] a, int key) {
    return binarySearch0(a, 0, a.length, key);
}
// Like public version, but without range checks.
private static int binarySearch0(int[] a, int fromIndex, int toIndex, int key) {
    int low = fromIndex;
    int high = toIndex - 1;

    while (low <= high) {
        int mid = (low + high) >>> 1;
        int midVal = a[mid];

        if (midVal < key)
            low = mid + 1;
        else if (midVal > key)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found.
}

// 找不到 key 则返回 -(应该出现的位置 + 1)
// mid 下标的不同写法,两种方式是等价的,第二种可以有效避免整数溢出
mid = (left + right) / 2; 
// left/2 + right/2 -> left - left/2 + right/2 -> left - (left - right)/2
mid = left + (right - left) / 2;
// 也可以使用位运算,效率更高,这里使用无符号右移要求左边必须是一个正数,普通右移没有限制
mid = (left + right) >>> 1;
// 二分查找的其他实现方式
while (l < r) {
    int mid = l + ((r - l) >> 1);
    if (check(mid)) {
        r = mid;
    } else {
        l = mid + 1;
    }
}

while (l < r) {
    int mid = l + ((r - l + 1) >> 1);
    if (check(mid)) {
        l = mid;
    } else {
        r = mid - 1;
    }
}

// 红蓝二分法
while (l + 1 != r) {
    int mid = l + ((r - l) >> 1);
    if (check(mid)) {
        l = mid;
    } else {
        r = mid;
    }
}

// 浮点数二分
// eps 表示精度,取决于题目对精度的要求,一般这里的精度比所求精度高 2 位
double eps = 1e-6; 
while (r - l > eps) {
    int mid = l + ((r - l) >> 1);
    if (check(mid)) {
        r = mid;
    } else {
        l = mid;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值