1. 基本排序方法
1. 冒泡排序
时间复杂度: O(nˆ2)
空间复杂度: O(1)
思想: 每次把最大的数,放到最后,就像水泡一样浮上去。
- 第一个循环控制每次的结尾end,从length-1到1,不用到0(第一个循环会到0)
- 第二个循环控制从 开头 到 倒数第二 的两两比较,下标表示为 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++,每次更新)
- 第一个循环控制每次的起始范围,从
i=0
开始,到i=length-2
结束 - 第二个循环开始时,每次暂时先让
minIndex = i
- 第二个循环控制从
j=i+1
到j=length-1
结束 的比较,每次比较minIndex
和arr[j]
如果arr[j] < minIndex
则minIndex
更新为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 对数器
对数器就是用一个绝对正确的排序算法和你自己的对比测试:
- 有一个你想要测的方法a
- 实现复杂度不好但是容易实现的方法b
- 实现一个随机样本产生器
- 把方法a和方法b跑相同的随机样本,看看得到的结果是否一样。
- 如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或者 方法b
- 当样本数量很多时比对测试依然正确,可以确定方法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 找到局部最小
题目描述:
在一个无序数组中,相邻元素不相等,请返回任意一局部最小值, 局部最小的定义是:
arr[0]<arr[1]
,则arr[0]
是局部最小arr[N-1]<arr[N-2]
,则arr[N-1]
是局部最小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; }