1. 基本排序方法

1. 冒泡排序

时间复杂度: O(nˆ2)
空间复杂度: O(1)
思想: 每次把最大的数,放到最后,就像水泡一样浮上去。

  1. 第一个循环控制每次的结尾end,从length-1到1,不用到0(第一个循环会到0)
  2. 第二个循环控制从 开头 到 倒数第二 的两两比较,下标表示为 i 和 i+1
    如果arr[i] > arr[i+1] 则交换两数
// 冒泡排序, in-place 修改
public static void bubbleSort(int[] arr){
    if(arr==null || arr.length<2){
        return;
    }
    // 第一个for loop规定比较的结束范围,从0~length-1 到 0~1
    for(int e = arr.length-1; e>0; e--){
        // 第二个loop 规定从0开始到end-1结束,依次比较 i 和 i+1的数,如果i大则交换
        for(int i=0; i<e; i++){
            if(arr[i] > arr[i+1]){
                swap(arr, i, i+1);
            }
        }
    }
}

2. 选择排序

时间复杂度: O(nˆ2)
空间复杂度: O(1)
思想: 从i=0开始,依次找到最小的数,并把它放到这个象征数组开始的位置(i++,每次更新)

  1. 第一个循环控制每次的起始范围,从i=0开始,到i=length-2结束
  2. 第二个循环开始时,每次暂时先让 minIndex = i
  3. 第二个循环控制从 j=i+1j=length-1 结束 的比较,每次比较 minIndexarr[j]
    如果arr[j] < minIndexminIndex 更新为j
public static void selectionSort(int[] arr){
    if(arr==null || arr.length<2){
        return;
    }
    // 最小的下标从 0 开始,到end-1结束
    for(int i=0; i < (arr.length-1); i++){
        int minIndex = i;
        // 第二个loop开始,依次比较,找到最小的下标
        for(int j = i+1; j<arr.length; j++){
            minIndex = arr[j] <= arr[minIndex] ? j : minIndex;
        }
        swap(arr,i,minIndex);
    }
}

3. 插入排序

时间复杂度: O(nˆ2), 最好情况O(n),最坏情况O(nˆ2)
空间复杂度: O(1)
思想: 首先假设数组在0-0上有序,且必有序(只有一个数), 依次插入一个数,依次与有序数组最外层的数比较,如果这个数更小,则交换,再比较,直到整体有序(再新插入数后)

public static void insertionSort(int[] arr){
    if (arr==null || arr.length<2){
        return;
    }

    // 假设0-0上有序, 从i=1开始加入新的数
    for(int i=1; i<arr.length; i++){
                // j是有序数组最外层的数,与新加入的数j+1比较
        for(int j=i-1; j>=0 ; j--){
                        // 最有情况: 每次新加的数,都大,不用交换
            if(arr[j] <= arr[j+1]){
                break;
            }
            swap(arr, j, j+1);
        }
    }
}

2. 注意的知识点

2.1 对数器

对数器就是用一个绝对正确的排序算法和你自己的对比测试:

  1. 有一个你想要测的方法a
  2. 实现复杂度不好但是容易实现的方法b
  3. 实现一个随机样本产生器
  4. 把方法a和方法b跑相同的随机样本,看看得到的结果是否一样。
  5. 如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或者 方法b
  6. 当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
// for test 对数器:绝对正确的排序
public static void comparator(int[] arr){
    Arrays.sort(arr);
}

// 产生随机数字
public static int[] generateRandomArray(int maxSize, int maxValue) {
        // Math.random()产生 0.0 - 1.0 的double type value
    int[] arr = new int[maxSize];
    for(int i=0; i<arr.length; i++){
        arr[i] = (int) (maxValue * Math.random());
    }
    return arr;
}

// 复制产生的数组
// 也可以用Arrays工具中的 Arrays.copyOf(array, newLength)

public static int[] copyArray(int[] arr){
    if (arr==null){
        return null;
    }
    int[] res = new int[arr.length];
    for(int i=0; i<arr.length; i++){
        res[i] = arr[i];
    }
    return res;
}

// 判断分类结果是否一致
// 也可以用Arrays工具中的 Arrays.equals(array1, array2)
public static boolean isEqual(int[] arr1, int[] arr2){
    if(arr1==null || arr2==null){
        return false;
    }
    if(arr1.length != arr2.length){
        return false;
    }
    for(int i=0; i<arr1.length; i++){
        if(arr1[i] != arr2[i]){
            return false;
        }
    }
    return true;
}

// 展示分类结果
public static void printArray(int[] arr){
    if(arr==null){
        return ;
    }
    for(int i=0; i<arr.length; i++){
        System.out.print(arr[i]+" ");
    }
    System.out.println();
}

最后测试的时候可以采用,随机生成5000次数组对比

int testTime =5000;
int maxSize =100;
int maxValue =100;
boolean succeed = true;
for(int i=0; i<testTime; i++){
    int[] arr1 = generateRandomArray(maxSize, maxValue);
    int[] arr2 = copyArray(arr1);

    bubbleSort(arr1);
    comparator(arr2);


    if(!isEqual(arr1, arr2)){
        succeed = false;
        break;
    }
    }
    System.out.println(succeed ? "Nice!" : "Error, check it!");

2.2 swap函数-java中的值传递和引用传递

public static void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

实验观察:
在java中,函数修改数组,和c++一样,是引用传递,直接修改本身,不产生copy;
但是函数修改数字,是值传递,不会修改本身的值;

结论:
参考资料1,2
更正参考资料1中,最后一张图,person 指向了 X03333 不是 X02222
在面试中,一句话总结就是: Java所谓引用传递也就是传递了引用的拷贝,所以本质也是值传递。
Persontest(Person person)函数中,实际传入不是main()中的p,而是在Persontest的栈帧中,创建了一个变量person,引用copy一份赋值给它了,在里面修改person的引用,让它指向别处,不会影响main中的p指向的class的实际的值。本质还是值传递

public class PassByValue {
    public static class Person{
        private String name;
        private int age;

        public String getName(){
            return name;
        }

        public int getAge(){
            return age;
        }

        public void setName(String name){
            this.name = name;
        }

        public void setAge(int age){
            this.age = age;
        }
    }

    public static void PersonTest(Person person){
        System.out.println("传入的person的name:"+person.getName());
        person.setName("我是张三");
        System.out.println("方法内重新赋值后的name:"+person.getName());
    }

    public static void main(String[] args) {
        Person p=new Person();
        p.setName("我是李四");
        p.setAge(45);
        PersonTest(p);
        System.out.println("方法执行后的name:"+p.getName());
    }
}

执行结果:

传入的person的name:我是李四
方法内重新赋值后的name:我是张三
方法执行后的name:我是张三

修改函数:

public static void PersonTest(Person person){
    System.out.println("传入的person的name:"+person.getName());
    person = new Person();
    person.setName("我是张三");
    System.out.println("方法内重新赋值后的name:"+person.getName());
}

执行结果:

传入的person的name:我是李四
方法内重新赋值后的name:我是张三
方法执行后的name:我是李四

3. 二分查找

二分查找,适用于在有序数组上查找符合某些标准的数,相比遍历它的时间复杂度是O(logn), 优化效果明显。
模版代码(来源leet code网友),注意边界条件:

public int searchInsert(int[] nums, int target) {
        int left = 0, right = nums.length - 1; // 注意
        while(left <= right) { // 注意
            int mid = (left + right) / 2; // 注意
            if(nums[mid] == target) { // 注意
                // 相关逻辑
            } else if(nums[mid] > target) {
                right = mid - 1; // 注意
            } else {
                left = mid + 1; // 注意
            }
        }
        // 相关返回值
        return 0;
    }

3.1 找 >= 某个数最左侧的位置在排序数组上

思路:

1.解法一:遍历

从头到尾遍历,找到>=某个数最左的位置
从尾到头遍历,找到<=某个数最右的位置
时间复杂度: O(n)

2.解法二: 二分查找

根据模版,注意=value时的操作,注意等号给谁了!
若合并到更新R,每次找到更小R,最后会找到最左的>=value的位置,也就是离你最近不小于你的数
若合并到更新L,每次找到更大L,最后会找到最右的<=value的位置,也就是离你最近不大于你的数
时间复杂度: o(log(N))

模版代码:

// 在排序的arr上,找到一个数的位置,它满足>=value的最左位置
// 如果没有找到,就是说这个数比数组中所有的数都大,则返回-1
public static int nearestIndexRight(int[] arr, int value) {
    int L = 0;
    int R = arr.length - 1;
    // 如果寻找的数大于最大的数,那么找不到他的位置,返回-1
    if (value > arr[R]) {
        return -1;
    }
    // 排除边界条件后
    while (L <= R) {
        int mid = L + ((R - L) >> 1);
        if (arr[mid] >= value) {
            R = mid - 1;
        } else {
            L =mid + 1;
        }
    }
    return L;
}

// 在排序的arr上,找到一个数的位置,它满足<=value的最右位置
// 如果没有找到,就是说这个数比数组中所有的数都小,则返回-1
public static int nearestIndexLeft(int[] arr, int value) {
    int L = 0;
    int R = arr.length - 1;
    // 如果寻找的数小于最小的数,那么找不到他的位置,返回-1
    if (value < arr[L]) {
        return -1;
    }
    // 排除边界条件后
    while (L <= R) {
        int mid = L + ((R - L) >> 1);
        // 等号的变化
        if (arr[mid] > value) {
            R = mid - 1;
        } else {
            L =mid + 1;
        }
    }
    return R;
}

3.2 找到局部最小

题目描述:
在一个无序数组中,相邻元素不相等,请返回任意一局部最小值, 局部最小的定义是:

  1. arr[0]<arr[1],则arr[0]是局部最小
  2. arr[N-1]<arr[N-2],则arr[N-1]是局部最小
  3. arr[i-1]>arr[i]<arr[i+1],则arr[i]是局部最小

此时二分法也可以用在无序数组上,若条件一和条件二不满足,则在i=1 到 i=N-2上寻找, 需要注意的是此时,在一个区间上先降后升,中间一定有拐点,也就是局部最小:

public static int getLessIndex(int[] arr) {
    // 极端情况
    if (arr == null || arr.length == 0) {
        return -1; // no exist
    }
    // 先检查第一个元素,是否符合
    if (arr.length == 1 || arr[0] < arr[1]) {
        return 0;
    }
    // 再检查最后一个元素,是否符合
    if (arr[arr.length - 1] < arr[arr.length - 2]) {
        return arr.length - 1;
    }
    // 在 1~N-2上寻找
    int left = 1;
    int right = arr.length - 2;
    int mid = 0;
    while (left <= right) {
        mid = (left + right) / 2;
        // 直接找到
        if(arr[mid] < arr[mid - 1] && arr[mid] < arr[mid + 1]){
            return mid;
        }
        // 则在右侧一定有拐点
        if (arr[mid] > arr[mid - 1]) {
            right = mid - 1;
        }
        // 则在左侧一定有拐点
        else if(arr[mid] > arr[mid + 1]){
            left = mid + 1;
        }
    }
    return left;
}

3.3 搜索插入位置

题目力扣35
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。

输入: [1,3,5,6], 5
输出: 2

输入: [1,3,5,6], 2
输出: 1

输入: [1,3,5,6], 7
输出: 4

输入: [1,3,5,6], 0
输出: 0

思路:
利用二分法,找插入位置,先处理两个边界情况,比最小的数小,或比最大的数大
剩下就是,直接找到target树,返回下标;或者找不到;
再找不到的情况中,一定是,target在两个数中间,比左边的大,但比右边的小,此时二分,mid指向右边的数,target比mid大,更新L=mid+1,这次迭代完成后,L=R只剩下一个数(比target大),再迭代最后一次,此时返回L或者R+1就是正确的index,因为在最后一次迭代,会更新R,L不动,让R=mid-1

代码:

public int searchInsert(int[] nums, int target) {
    //二分法插入,套模版
    // 边界情况,比最小的数小
    if(nums[0]>target){
        return 0;
    }
    // 边界情况,比最大的数大
    if(nums[nums.length-1]<target){
        return nums.length;
    }
    int L = 0;
    int R = nums.length-1;
    while(L<=R){
        int mid = L + ((R-L)>>1);
        if(nums[mid]==target){
            return mid;
        }else if(nums[mid]<target){
            L=mid+1;
        }
        else{
            R=mid-1;
        }
    }
    return R+1 or L;
}