导学
程序=算法+数据结构
二分法查找
升序排列的数字:先将索引放在first和end上,找见小于等于middle的索引,当middle对应值大于goal时,将end放在middle-1上
public class test1 { public static void main(String[] args){ int[] arr = {1,3,5,7,9,11,13}; int result = dichotomy(arr,11); System.out.println(result); } public static int dichotomy(int[] arr,int result){ int first = 0,end = arr.length - 1; while (first <= end ){ //当要查找的数在第一位时,会出现first=end ,此时若条件为(first<end)则会停止查询,直接返回-1,如下图 int middle = (first + end)/>>>1; //当end取到正整数最大值时,假设result在右侧,则会出现first+end超出正数范围的数,就会成为负数,正常数学计算就是数轴形式,但是java里面第一位视为符号位,是一个闭合圆形 ,所以用到了无符号右移,右移一次就是除以2的效果 //int middle = (first + end)>>>1; if(arr[middle] > result){ end = middle -1; } else if(arr[middle] < result){ first = middle + 1; }else{ return middle; } } return -1; } }
public class test2 { public static void main(String[] args){ int[] arr = {1,3,5,7,9,11,13}; int result = redichotomy(arr,11); System.out.println(result); } public static int redichotomy(int[] arr,int result){ int first = 0,end = arr.length; while (first < end ){ int middle = (first + end)>>>1; if(arr[middle] > result){ end = middle; } else if(arr[middle] < result){ first = middle + 1; }else{ return middle; } } return -1; } } //第二种就是一个修改,第一种要注意边界,第二种要注意没有的值,假如10,first=end时就会存在死循环
二分查找平衡版
因为result在最左边时,只执行第一个if,但是在最右边时,会执行第二个if,导致左边执行L次,右边执行2L次,左右不平衡,
public static int reDichotomy(int[] arr, int result){ int first = 0,end = arr.length; while (1 < end - first ){ int middle = (first+end)>>>1; if(result < arr[middle]){ end = middle; }else { first = middle; } } if(arr[first] == result){ return first; }else { return -1; } } //优点:平均比较次数减少,适合多数据使用 //缺点:最优情况不存在
Java二分法逻辑
//没有找到的返回值是-(插入点+1) 插入点就是first的值 +1的原因就是当first=0时在java里面0和-0无法区分
数组拷贝
import java.util.Arrays; public class test3 { public static void main(String[] args) { int[] arr = {1,3,5,7,9,11,13}; int result = 8; int i = Arrays.binarySearch(arr,result); if(i<0){ int index = Math.abs(i+1); int[] brr = new int[arr.length+1]; System.arraycopy(arr,0,brr,0,index); brr[index] = result; System.arraycopy(arr,index,brr,index+1,arr.length-index); System.out.println(Arrays.toString(brr)); } } }
元素重复
public static int findCandidate(int[] arr,int result){ int first = 0,end = arr.length - 1; int Candidate = -1; while (first <= end ){ int middle = (first + end)>>>1; if(arr[middle] > result){ end = middle -1; } else if(arr[middle] < result){ first = middle + 1; }else{ Candidate = middle; end = middle -1; //右侧时改first就可以了 //first = middle + 1; } } return Candidate; }
//给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。 //如果数组中不存在目标值 target,返回 [-1, -1]。 class Solution { public int[] searchRange(int[] nums, int target) { int low = 0; int high = nums.length - 1; while (low <= high) { int mid = (low+high)>>>1; if (nums[mid] == target) { //找到中间值的时候,就让左右两边开始缩小范围 while (nums[low] != target) { low++; } //左边找的就是左边界 while (nums[high] != target) { high--; } //右边找的就是右边界 return new int[] { low, high }; //这里找到之后就是返回左边界和右边界 } else if (nums[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return new int[] { -1, -1 }; } }
//给你一个非负整数 x ,计算并返回 x 的 算术平方根 。 //由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。 //注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 class Solution { public int mySqrt(int x) { int first = 0 ,end = x; int result = -1; while(first <= end){ int middle = (first + end)>>>1; if((long)middle*middle <= x){ result = middle; first = middle + 1; }else{ end = middle - 1; } } return result; }
//给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。 //完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。 //不能使用任何内置的库函数,如 sqrt class Solution { public boolean isPerfectSquare(int num) { int first = 0 , end = num ; while(first <= end){ int middle = (first+end)>>>1; if((long) middle*middle < num){ first = middle + 1; }else if((long) middle*middle>num){ end = middle - 1; }else{ return true; } } return false; } }
单侧重复,改进返回值
//返回-1没有什么实际含义,返回first就是返回大于result的最近元素的索引 public static int findCandidate(int[] arr,int result){ int first = 0,end = arr.length - 1; while (first <= end ){ int middle = (first + end)>>>1; if(arr[middle] >= result){ //<= end = middle -1; } else{ first = middle + 1; } } return first; //return first + 1; //向左查找的时候,如果存在,就是查到最左侧的重复项,向右查找就是查到最右侧的重复项 //如果不存在,向左查找大于result的最小值,向右查找小于result的最大值 }
两侧重复,有重有漏(对于正中间来说)
class Solution { public int countTarget(int[] nums, int target) { if (nums.length == 0) return 0; return binarySearch(nums, target + 1) - binarySearch(nums, target); } private int binarySearch(int[] nums, int target) { int left = 0, right = nums.length; while (left < right) { int mid = (left + right)>>>1; if (nums[mid] < target) { left = mid + 1; } else { right = mid; } } return right; } }
如何衡量算法的好坏
事前分析法:
1.分析算法最差的情况,比较运算速率,
2.假设每行语句执行时间一样
//线性查找 public static int LinearSearch(int[] arr,int result){ for (int i = 0; i < arr.length; i++) { if(result == arr[i]){ return i; } } return -1; } //运行时间为3n+3次
public static int dichotomy(int[] arr,int result){ int first = 0,end = arr.length - 1; while (first <= end ){ int middle = (first + end)>>>1; if(arr[middle] > result){ end = middle -1; } else if(arr[middle] < result){ first = middle + 1; }else{ return middle; } } return -1; } //运行时间:假设有八个元素,运行4次,16个元素,运行5次,实际次数为(log2*n)*5+4
数据少的时候线性查找好用,数据多的时候,二分查找好用
时间复杂度
一个算法的执行,随着数据规模扩大,而增长的时间成本
不依赖于环境因素
大O表示法:O(n)
渐近上界就是代码执行的最差情况,渐近下界就是代码执行的最优情况
渐近紧界:既能代表算法的最差情况也能代表最优情况
找见原函数的最高项拿出来,然后改变系数
空间复杂度
与时间复杂度类似,一般也使用大O表示法,一个算法执行随数据规模增大,而增长的额外空间成本
public static int dichotomy(int[] arr,int result){ int first = 0,end = arr.length - 1; while (first <= end ){ int middle = (first + end)>>>1; if(arr[middle] > result){ end = middle -1; } else if(arr[middle] < result){ first = middle + 1; }else{ return middle; } } return -1; } //三个int占了十二字节,middle会数据覆盖,所以就不会产生新的空间
数组
假设知道了数组的起始位置,就可以计算出他的存储位置。
-
起始地址BaseAddress
$$
BaseAddress + i*size
$$
-
i是索引,size是每个元素占用字节
空间占用
-
8字节markword
-
4字节类指针
-
4字节数组大小(决定了数组最大容量是2的32次方)
-
数组元素+对齐字节(不足的要补齐)
-
8+4+4+5*4+4
随机访问
根据索引查找元素,时间复杂度是O(1)
package array; public class Array1 { private int size = 0; private int volume = 8;//数组容积 private int[] array = new int[volume]; //添加元素 public void addLast(int element){ add(size,element); } //添加元素,插入元素 public void add(int index , int element){ if(index<0||index>size){ System.out.println("Out of index range"); }else{ System.arraycopy(array,index,array,index+1,size-index); } array[index]=element; size++; } //查询元素 public int get(int index){ return array[index]; } }
数组遍历
package array; import java.util.Arrays; import java.util.Iterator; import java.util.function.Consumer; import java.util.stream.IntStream; public class Array1 implements Iterable<Integer> { private int size = 0; private int volume = 8;//数组容积 private int[] array = new int[volume]; public void addLast(int element){ add(size,element); } public void add(int index , int element){ if(index<0||index>size){ System.out.println("Out of index range"); }else{ System.arraycopy(array,index,array,index+1,size-index); } array[index]=element; size++; } //1.普通遍历 public int get(int index){ return array[index]; } //2.Consumer public void foreach(Consumer<Integer> consumer){ for (int i = 0; i < array.length; i++) { consumer.accept(array[i]); } } //3.迭代器遍历 @Override public Iterator<Integer> iterator() { return new Iterator<Integer>(){ int i = 0; @Override public boolean hasNext() {//判断有没有下一个元素 return i < size; } @Override public Integer next() {//返回当前元素,并且移动到下一个元素 return array[i++]; } }; } //4.转换成流来遍历 public IntStream stream(){ return IntStream.of(Arrays.copyOfRange(array,0,this.size));//size含头不含尾 } }
package array; public class test1 { public static void main(String[] args){ Array1 array1 = new Array1(); for(int i = 1; i < 6 ; i++){ array1.addLast(i); } array1.add(1,6); array1.forEach(System.out::println); for (Integer element : array1) { System.out.println(element); } array1.stream().forEach(System.out::println); } }
删除元素
双指针其实找的并不是重复项,而是寻找不重复内容,让其组成新数组
双指针指定删除
class Solution { public int removeElement(int[] nums, int val) { int length = nums.length; if(length == 0){ return 0; } else{ int left =0; for(int right = 0;right<length;right++){ if(nums[right] != val){ nums[left] = nums[right]; left++; } } return left; } } }
//给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 class Solution { public void moveZeroes(int[] nums) { if(nums.length == 0){ return; } int i = 0 , j = 0 ; for(j = 0 ; j < nums.length ; j++){ if(nums[j] != 0){ nums[i] = nums[j]; i++; } } //上半部分就是双指针移动元素,下半部分就是从数组最后一个非零整数开始将他后面的元素都变成0 for(int k = i ; i < nums.length ; i ++){ nums[i] = 0; } } }
双指针删除重复项
class Solution { public int removeDuplicates(int[] nums) { int length = nums.length; if(length ==0){ return 0; }else{ int left = 1; for(int right = 1;right<length;right++){ if(nums[right-1] != nums[right]){ nums[left] = nums[right]; ++left; } } return left; } } }
给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。
class Solution { public boolean backspaceCompare(String s, String t) { return change(s).equals(change(t)); } private String change(String string){ //使用栈的方式来解决,思路就是:当字符不为#的时候,就加入栈,若为#,先判度栈是否为空,若不为空就删除栈总长度的前一个元素,就是#之前的元素,两个栈处理之后转换成字符串进行比较 StringBuffer ret = new StringBuffer(); int length = string.length(); for(int i = 0 ; i < length ; i ++){ char c = string.charAt(i); if(c != '#'){ ret.append(c); }else{ if(ret.length() > 0){ ret.deleteCharAt(ret.length()-1); } } } return ret.toString(); } }
两数求和
class Solution { public int[] twoSum(int[] nums, int target) { int length = nums.length; if(length == 0 ){ return new int[0]; } for(int i = 0;i<length;i++){ for(int j = i+1;j<length;j++){ if((nums[i] + nums[j]) == target){ return new int[]{i,j}; } } } return new int[0]; } }
java数组快速复制删除
public int delete(int index){ int deleted = array[index]; if(deleted >=0 &&deleted < size-1){ System.arraycopy(array,index + 1,array,index,size-index-1); }else { return -1; } return deleted; }
//这两个都是用来比较数组的API但是比较之前需要加入maven下的两个依赖 assertEquals(3,delete); assertIterableEquals(List.of(1,2,4,5),array1);
加一问题
-
如果最后一位不是9,那么直接+1
-
如果最后一位是9,且前面有不是9的元素,那么从后向前第一个不是9的元素+1,之后的元素全部变为0
-
如果整个数组都是9,那么创建一个新数组,让第一个元素为1,之后的全部为0
class Solution { public int[] plusOne(int[] digits) { int n = digits.length; if(digits[n-1] != 9){ digits[n-1]++; return digits; } for(int i = n - 1 ; i >= 0 ; i -- ){ if(digits[i] != 9){ digits[i] ++; for(int j = i + 1 ; j < n ; j ++ ){ digits[j] = 0; } return digits; } } int[] arr = new int[n + 1]; arr[0] = 1; for(int m = 1 ; m < n + 1;m++){ arr[m] = 0; return arr; } }
数组合并
class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { for(int i = 0 ; i < n ; i++,m++){ nums1[m] = nums2[i]; } sort(nums1,nums1.length-1); } private void sort(int[] nums1 , int end){ if(end == 0 ){ return; } int x = 0; for(int i = 0; i < end ; i++){ if(nums1[i+1] < nums1[i]){ int middle = nums1[i]; nums1[i] = nums1[i+1]; nums1[i+1] = middle; x = i; } } sort(nums1,x); } }
滑动窗口
//给定一个含有 n 个正整数的数组和一个正整数 target 。 //找出该数组中满足其总和大于等于 target 的长度最小的 连续 //子数组 // [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 class Solution { public int minSubArrayLen(int target, int[] nums) { int result = Integer.MAX_VALUE ; int sum = 0 ; for(int i = 0 , j = 0 ; j < nums.length ; j++){ sum += nums[j]; while(sum>=target){ result = Math.min(result, j - i + 1); sum-=nums[i++]; } } return result == Integer.MAX_VALUE ? 0 : result; } }
class Solution { public int totalFruit(int[] fruits) { int[] window = new int[fruits.length + 1]; int l = 0 , r = 0 , ans = 0 , same = 0; while(r < fruits.length){ if(window[fruits[r++]]++ == 0)same++; while(same > 2){ if(window[fruits[l++]] -- == 1)same--; } ans = Math.max( r - l , ans ); } return ans; } }
二维数组
int[][] array = { {1,2,3}, {4,5,6}, };
-
二维数组占用20字节
-
一维数组占用32字节
-
他们在内存上都是连续的
缓存和局部性原理
缓存:内存和CPU的传输速度相差太多,所以有了缓存让数据暂时存在缓存中,而缓存的速度接近CPU,让CPU从缓存中读取
空间局部性:缓存在内存中读取时,将数据转换成缓存行,一个缓存行是64个字节
先行再列:缓存可以将一行数据读入,并且CPU在读取时,是已经读入的
先列再行:缓存是可以读入,但是CPU读取是发现相邻单位并没有被读入,而是下一行的元素同列元素被读入
螺旋矩阵
// class Solution { public int[][] generateMatrix(int n) { int i , j;//i,j是两个指针 int[][] arr = new int[n][n]; int count = 1;//count就是要添加的元素 int loop = 0 ;//loop就是循环的次数 int start = 0 ;//start就是每次循环起始的位置 while(loop++ < n / 2){ //一分为2,所以遍历结束的条件就是n/2 //先确定每次添加元素的结束条件,一般是左闭右开 for(j = start ; j < n - loop ; j++){ arr[start][j] = count++; } //横向正向添加 for(i = start ; i < n - loop ; i++){ arr[i][j] = count++; } //纵向正向添加,虽然添加到行最后一位的前一位,但是指针实际上等于最后一位才停止,所以里面不需要起始条件 for( ; j > start ; j-- ){ arr[i][j] = count++; } //横向逆向添加 for( ; i > start ; i-- ){ arr[i][j] = count++; } //纵向逆向添加 //每添加完一组,就将起始位置做一次更改 start++; } //当矩阵是奇数项的时候,需要向正中间添加元素,就是两个start的索引处 if( n % 2 == 1 ){ arr[start][start] = count; } return arr; } }
//给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素 //思路:将一个二维数组从左至右,从上到下,从右向左,从下至上,依次螺旋添加进入集合,最后返回集合 class Solution { public List<Integer> spiralOrder(int[][] matrix) { //创建一个集合 List<Integer> list = new ArrayList<Integer>(); //判断数组长度,宽度是否为空 if(matrix == null || matrix.length == 0 || matrix[0].length == 0){ return list; } //下面四个是四个边界,分别是最左边,最右边,最上边,以及最下边 int left = 0 , right = matrix[0].length - 1 , top = 0 , bottom = matrix.length - 1; while(left <= right && top <= bottom){ //让列指针等于最左边,并且遍历到最右边,然后行指针不动,依次加入集合 for(int column = left ; column <= right ; column++){ list.add(matrix[top][column]); } //让行指针不动,列指针从最上遍历至最下, for(int row = top + 1 ; row <= bottom ; row++){ list.add(matrix[row][right]); } if(left < right && top < bottom) { //让行指针不动,列指针依次减小 for(int column = right - 1 ; column > left ; column--){ list.add(matrix[bottom][column]); } //让列指针不动,行指针依次减小 for(int row = bottom ; row > top ; row--){ list.add(matrix[row][left]); } } //循环一次后,让范围缩小一圈 left++; right--; top++; bottom--; } //zui'hou return list; } }