数组总结:双指针,有序二分,分治;
1.找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
重点是:使用set集合,set中不能储存重复的值
题目链接:力扣
class Solution {
public int findRepeatNumber(int[] nums) {
//使用set集合,set中不能储存重复的值
Set<Integer> set = new HashSet<Integer>();
for(int num : nums){
if(set.contains(num)) return num;
set.add(num);
}
return -1;
}
}
2.在有本身性质的二维数组中,查找值。
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
题目链接:力扣
根据其递增的性质,将正方形立起来,像是二叉树,选取左下角作为遍历的开始。元素向上,值小于该元素,元素向右,值大于该元素
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
//从数组的最后一行的第一个元素开始搜查找,元素所在的列以上的值小于该元素,元素右边行的值大于该元素
//1.当遍历到元素时,该元素的值大于target时,行--;元素值小于target,列++
int i = matrix.length-1;
int j = 0;
while(i>=0 && j<matrix[0].length){
if(matrix[i][j]>target) i--;
else if(matrix[i][j]<target) j++;
else return true;
}
return false;
}
}
3.旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。
题目链接:力扣
方法一:暴力查找
class Solution {
public int minArray(int[] numbers) {
int min = numbers[0];
for(int i= 1; i<numbers.length;i++){
if(numbers[i]<min){
min = numbers[i];
}
}
return min;
}
}
方法二:二分法
class Solution {
public int minArray(int[] numbers) {
int i =0;
int j = numbers.length-1;
//二分法:此数组从开始到旋转点是递增,从旋转点到末尾也是递增,左递增数组,右递增数组,所以用二分法
//选取中间值和区间的最右边的值比较,
//跳出循环时i=j
while(i<j){
int m = (i+j)/2;
if(numbers[m]>numbers[j]) i = m+1;
else if(numbers[m] <numbers[j]) j =m;
else j--;
}
return numbers[j];
}
}
4.矩阵中的路径(DFS+剪枝)
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
题目链接:力扣
题目分析:本题是典型的矩阵搜索问题,可以使用深度优先搜索(DFS)+ 剪枝
- 深度优先搜索:可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
- 剪枝: 在搜索中,遇到
这条路不可能和目标字符串匹配成功
的情况(例如:此矩阵元素和目标字符不同、此元素已被访问),则应立即返回,称之为可行性剪枝
。
算法流程:
- 递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。
- 终止条件:
返回 false: (1) 行或列索引越界 或 (2) 当前矩阵元素与目标字符不同 或 (3) 当前矩阵元素已访问过 ( (3) 可合并至 (2) ) 。
返回 true : k = len(word) - 1 ,即字符串 word 已全部匹配。
- 递推工作:
标记当前矩阵元素: 将 board[i][j] 修改为 空字符 '' ,代表此元素已访问过,防止之后搜索时重复访问。(当进行上下左右同时搜索时,有可能之前的元素,已经被访问过,所以要把以访问的元素置空,当递归回溯时,再修改为原来的数组值)
搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res 。
还原当前矩阵元素: 将 board[i][j] 元素还原至初始值,即 word[k] 。
- 返回值: 返回布尔量 res ,代表是否搜索到目标字符串。
class Solution {
public boolean exist(char[][] board, String word) {
char[] chars = word.toCharArray();
//确定单词word的起始位置
for(int i = 0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
if(dfs(board,i,j,0,chars)) return true;
}
}
return false;
}
//k表示寻找第几个word中的字符
public boolean dfs(char[][] board,int i,int j,int k,char[] chars){
//递归回溯的条件
if(i<0 || i == board.length || j<0 || j== board[0].length || board[i][j] != chars[k]) return false;
if(k == chars.length-1) return true;
//继续递归条件,说明此时board[i][j]=chars[k]
//此时将board[i][j]做遍历标记
board[i][j]='\0';//这表示空字符
//继续向该元素的上下左右递归
boolean res = dfs(board,i-1,j,k+1,chars) || dfs(board,i+1,j,k+1,chars) ||
dfs(board,i,j-1,k+1,chars) || dfs(board,i,j+1,k+1,chars);
board[i][j]=chars[k];
return res;
}
}
5.机器人的运动范围-(DFS+剪枝)
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
题目链接:力扣
此题与上题的区别:
- 递归时指向右和下递归,不用再判断左上,每回溯一次加1;
- 算i,j的和时,算法,圆圈加点表示求余
class Solution {
//初始化矩阵
int m,n;
// int[][] arr;//定义--这个数组没用,因为已经知道长度了
int k;
boolean[][] value;//用于记录哪个格子被记录过
public int movingCount(int m, int n, int k) {
this.m = m;
this.n = n;
// arr = new int[m][n];//初始化
this.k = k;
value = new boolean[m][n];
return dfs(0,0,0,0);
}
//深度优先遍历+剪枝
public int dfs(int i,int j,int si,int sj){
//递归回溯条件
if(i ==m || j==n || si + sj > k || value[i][j]) return 0;//此时这个递归值为0;
//设置ij点被遍历
value[i][j] = true;
return 1+dfs(i+1,j,(i+1) %10 ==0 ?si-8:si+1,sj)+dfs(i,j+1,si,(j+1) %10 ==0 ?sj-8:sj+1);
}
}
6.数组中次数超过一半的数
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2] 输出: 2
方法一:HashMap
方法二:排序
方法三:摩尔投票法
//方法一:哈希表
class Solution {
int n;
public int majorityElement(int[] nums) {
Map<Integer,Integer> map = new HashMap<>();
for(int num:nums){
// if(map.get(num) == null) map.put(num,1);
// else map.put(num,map.get(num)+1);
map.put(num,map.getOrDefault(num,0)+1);
if(map.get(num)>nums.length>>1) return num;
}
return 0;
}
}
//方法二:排序法
class Solution {
int x=0;//用于记录最终的x出现次数超过一半的值
public int majorityElement(int[] nums) {
//投票法:不同就会为负值,题目中数超过数组的长度一半,最后剩下这个数字
int vote =0;
for(int num : nums){
if(vote == 0) x = num;
vote += x==num ? -1:1;
}
return x;
}
}
7.最小的k个数
输入整数数组 arr
,找出其中最小的 k
个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
题目链接:力扣
方法一:用数组自带的排序Arrays.sort(arr);
方法二: 快排的改进
方法三:堆排序(这里也是用的自带的堆排序函数)
方法一:快排
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
//基于快速排序的,找到第k+1个最小值,那么从数组的最左边开始到k-1索引处
//正好是确定值,基于快速排序改进,不用向左右连续递归
if(k >= arr.length) return arr;
quickSort(arr,k,0,arr.length-1);
return Arrays.copyOf(arr,k);
}
public void quickSort(int[] arr,int k,int l,int r){
int i = l;
int j = r;
//参考值是arr[l]
while(i<j){
//这里是等于,防止了死循环,这里好像必须先向再右边寻找
while(i<j && arr[j]>=arr[l]){
j--;
}
while(i<j && arr[i]<=arr[l]){
i++;
}
swap(arr,i,j);
}
swap(arr,i,l);//这里防止栈溢出
//如果划分值arr[l],l=k,此时划分完左边是最小的k个数
if(k>i) quickSort(arr,k,i+1,r);
if(k<i) quickSort(arr,k,l,i-1);
return;//这里k=i时退出循环
}
public void swap(int[] arr,int i,int j){
int tem = arr[i];
arr[i] = arr[j];
arr[j]= tem;
}
}
8.把数组排成最小的数
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
输入:[3,30,34,5,9]
输出:"3033459"
题目链接:力扣
解析:
此题求拼接起来的最小数字,本质是一个排序问题。设数组nums中任意两数字的字符串为x和y,
则判断顺序为(最小数字)
- 若x+y>y+x,则x"大于"y,要求最小数,此时y应该排在x前面
- 反之,x"小于“y,此时应该排在x的y前面。
算法流程:
1.初始化:字符串列表strs,保存各数字的字符串格式
2.应用以上”排序判断规则“,对strs执行排序;
3.返回值:拼接strs中的所有字符串,并返回
class Solution {
public String minNumber(int[] nums) {
//1.将数组转为字符数组
String[] strs = new String[nums.length];
for(int i =0;i<nums.length;i++){
strs[i]=String.valueOf(nums[i]);
}
//2.快排
quickSort(strs,0,strs.length-1);
//3.将字符数组转为字符串,此时借助stringbuffer
StringBuilder res = new StringBuilder();
for(String num:strs){
res.append(num);
}
return res.toString();
}
//根据本题特定的规则进行快排
public void quickSort(String[] strs,int l,int r){
//跳出递归的条件
if(l>=r) return;
int i = l;
int j = r;
String tem=strs[i];
while(i<j){
while(i<j && (strs[j]+strs[l]).compareTo(strs[l]+strs[j])>=0) j--;
while(i<j && (strs[i]+strs[l]).compareTo(strs[l]+strs[i])<=0) i++;
//交换两个值
tem = strs[i];
strs[i]=strs[j];
strs[j]=tem;
}
//交换l,i值
strs[i]=strs[l];
strs[l]=tem;
//向左右递归
quickSort(strs,l,i-1);
quickSort(strs,i+1,r);
}
}
8.一个整型数组 nums
里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
题目链接:力扣
按位于 & ,这里要加括号
class Solution {
public int[] singleNumbers(int[] nums) {
//异或规律 nums中的值异或结果是:剩下两个不同的数字异或
int res = 0;//最后异或结果
int m = 1;//用于移位的数
int x =0;
int y=0;
//遍历数组提取出异或值
for(int num : nums){
res = res^num;
}
//寻找从右开始寻找第一个res为1的位
while((res & m) ==0){//这里只能是判断是不是0,
m=m<<1;//1左移
}
for(int num : nums){
if((num & m)==0) x=x^num;
else y=y^num;
}
return new int[]{x,y};
}
}
9.在一个数组 nums
中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
题目链接:力扣
方法一:map
class Solution {
public int singleNumber(int[] nums) {
//用哈希表
Map<Integer,Integer> map = new HashMap<>();
for(int num:nums){
if(map.get(num) !=null){
map.put(num,map.get(num)+1);
}else{
map.put(num,1);
}
}
//遍历map数组
for(Map.Entry<Integer,Integer> entry : map.entrySet()){
if(entry.getValue() ==1) return entry.getKey();
}
return 0;
}
}
方法二:根据3个数相同,统计二进制数对应位数 1的个数
class Solution {
public int singleNumber(int[] nums) {
//利用所有数字出现了3次为标准
//根据题目要求 最长是31为
int[] counts = new int[32];
//统计所有数字每位 1 出现的次数,
for(int i =0;i<nums.length;i++){
//统计nums[i]在相应位置出现的次数
for(int j =0;j<32;j++){
counts[j] += nums[i] & 1;//是从低位开始的
//无符号右移
nums[i] >>>=1;
}
}
int res =0;
//对counts中的每个值对3取余
for(int i =0;i<32;i++){
res<<=1;//左移一位恢复值;
res |= counts[31-i]%3;//高位倒着来,或运算是加入数组的1
}
return res;
}
}
10.输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
题目链接:力扣
双指针遍历
class Solution {
public int[] twoSum(int[] nums, int target) {
int r = 0;
int i=0;
for(; i<nums.length;i++){
if(nums[i]>=target){
r = i;
break;
}
}
if(i==nums.length) r=nums.length-1;
//int r = nums.length-1;
int l =0;
while(r>l){
if(nums[l]+nums[r]==target){
return new int[]{nums[l],nums[r]};
}else if(nums[l]+nums[r]>target){
r--;
}else{
l++;
}
}
return null;
}
}
11.分治思想:也算是动态规划,n*n矩阵,上下三角
给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
题目链接:力扣
class Solution {
public int[] constructArr(int[] a) {
//体现了分治的思想
if(a.length ==0) return new int[0];
//先计算0到i-1的结果记为b[i],再乘i+1到n的连乘
//b[i]结果是下三角
int[] b = new int[a.length];
b[0]=1;
//计算下三角乘积
for(int i=1;i<a.length;i++){
b[i] = b[i-1] * a[i-1];
}
//下三角和上三角乘
int tmp=1;
for(int i=a.length-2;i>=0;i--){
tmp *=a[i+1];//用一个值来统计
b[i]=tmp *b[i];
}
return b;
}
}
12.发现规律:最大值和最小值的差是小于5的
从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
题目链接:力扣
class Solution {
public boolean isStraight(int[] nums) {
//本题大小王可以看成任意的数字,最大值和最小值不会超过5;
//排序加遍历
//统计0的数量
int zero = 0;
Arrays.sort(nums);
for(int i =0;i<nums.length-1;i++){//这里i的最大值减1
//判断会不会重复
if(nums[i]==nums[i+1] && nums[i] !=0 ) return false;
if(nums[i]==0) zero++;
}
return nums[4]-nums[zero]<5;
}
}