文章目录
查找算法
二分法
while循环结束条件是l>=r,为什么二分结束时我们优先取r而不是l?
循环结束的时候l>r 此时可能发生越界,因此二分问题优先取r不会翻车。
offer03数组找重复
需求
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
示例
输入: [2, 3, 1, 0, 2, 5, 3] 输出:2 或 3
方法1:hashset集合
class Solution {
public int findRepeatNumber(int[] nums) {
//使用set集合的不存储重复元素的性质
Set<Integer> set = new HashSet<Integer>();
int result = 0;
for(int num : nums){
if(!set.add(num))
result = num;
}
return result;
}
}
方法2:双层for循环
class Solution {
public int findRepeatNumber(int[] nums) {
// 双层for循环遍历
int result = -1;
for(int i = 0; i < nums.length-1 ; i++){
for(int j = i+1 ; j < nums.length;j++){
if(nums[i] == nums[j]){
result = nums[i];
break;
}
}
if(result != -1)
break;
}
return result;
}
}
方法3:数组重排
分析
将数组重新排列成,数组下标与数组内容相同的形式。
下标i 对应的值num[i] 判断是否相等,相等则继续判断下一个;
不相等,判断下标num[i] 与num[num[i]] 的值是否相等,相等,则说明有重复元素输出nums[i];
不相等,则交换两个数。
class Solution {
public int findRepeatNumber(int[] nums) {
int i = 0 ;
int temp = -1;
while( i < nums.length){
if(nums[i] == i){
i++;
continue;
}
if(nums[i) == nums[nums[i]]) return nums[i];
temp = nums[i];
nums[i] = nums[temp];
nums[temp] = temp;
}
return -1;
}
}
图示
offer53 在排序数组中查找数字I
需求
统计一个数字在排序数组中出现的次数。
示例
输入: nums = [5,7,7,8,8,10], target = 8 输出: 2
输入: nums = [5,7,7,8,8,10], target = 6 输出: 0
方法1:循环对比
class Solution {
public int search(int[] nums, int target) {
int i = 0 ;
int count = 0;
while(i < nums.length){
if(nums[i] < target){
i++;
continue;
}else if(nums[i] == target){
i++;
count++;
continue;
}else{
break;
}
}
return count;
}
}
方法2:Map集合
class Solution {
public int search(int[] nums, int target) {
// 使用Map集合
Map<Integer,Integer> map = new HashMap<>();
map.put(target,0);
int value = -1;
for(int num:nums){
if(map.containsKey(num)){
value = map.get(num) + 1 ;
map.put(num,value);
}
}
return map.get(target);
}
}
方法3:二分法(找右节点)
class Solution {
public int search(int[] nums, int target) {
return searchBorder(nums,target) - searchBorder(nums,target-1);
}
public int searchBorder(int[] nums,int target){
int left = 0;
int right = nums.length-1;
int mid = 0;
while(left <= right){
mid = (right + left) /2;
if(nums[mid] <= target)
left = mid+1;
else
right = mid -1;
}
return right;
}
}
方法4:二分法(找左右节点)
在这里插入代码片class Solution {
public int search(int[] nums, int target) {
// 二分法 找左边界 右边界
int i = 0 , j = nums.length-1;
int mid = 0;
while( i <= j){
mid = (i+j)/2;
if(nums[mid] < target)
i = mid + 1;
else
j = mid -1;
}
int left = j;
// 数组无target情况
if( i <= nums.length-1 && nums[i] != target) return 0;
// 找右节点
i = 0;
j = nums.length-1;
while(i <= j){
mid = (i+j)/2;
if(nums[mid] <= target)
i = mid+1;
else
j = mid -1;
}
int right = i;
return right - left - 1 ;
}
}
图示
offer53II 0~n-1中缺失的数字
需求
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
示例
输入: [0,1,3] 输出: 2
输入: [0,1,2,3,4,5,6,7,9] 输出: 8
方法1:循环比较
通过比较元素与对应的下标 是否一样,不一样返回下标;
都一样,说明没有数组长度n这个元素,返回n。
class Solution {
public int missingNumber(int[] nums) {
// 遍历数组比较
int n = nums.length;
for(int i = 0 ; i < n ; i++){
if(i != nums[i]) return i;
}
return n;
}
}
时间复杂度是:O(n) for循环查找;
空间复杂度是:O(n) nums数组的长度。
方法2:数组之和(数字)
求数组元素之和、数组长度n的1-n之间的元素之和。两个相减就是少的元素。
class Solution {
public int missingNumber(int[] nums) {
// 求数组长度n的元素之和
int n = nums.length;
int sum0=0,sum1 = 0;
// 求1..n的和
sum0 = n*(n+1)/2;
// 求给定数组元素之和。
for(int num :nums)
sum1 += num;
return sum0-sum1;
}
}
方法3:位运算
采用异或的形式,相同元素为0,不同为1.
由于添加元素0-n 与数组元素进行异或,缺失的元素是一个,则会被保留下来。
class Solution {
public int missingNumber(int[] nums) {
// 位运算
int result = 0;
// 数组元素异或
for(int i = 0 ; i < nums.length ; i++)
result ^= nums[i];
// 0-n之间元素异或
for(int i = 0; i < nums.length+1 ; i++)
result ^= i;
return result;
}
}
方法4:二分法
分析
数组被分为左子数组、右子数组,缺失的数字是右子数组的首个元素对应的索引x
class Solution {
public int missingNumber(int[] nums) {
// 二分法
int i = 0 , j = nums.length-1;
int mid = 0;
while(i <= j){
mid = (i+j) /2;
if(nums[mid] == mid) // 说明在右侧
i = mid +1 ;
else // 说明在左侧
j = mid -1 ;
}
return j+1;
}
}
时间复杂度:O(logN)
空间复杂度:O(N)
offer04 二维数组中的查找
需求
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定target = 5 返回true;
给定target = 20,返回false;
方法1:for循环+break
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
// 特殊值
// 这个位置需要先判断matrix是否为null 如果直接计算length 可能会出错,比如它是一维数组且为空 那么matrix[0].length会报错。
if(matrix == null || matrix.length == 0 || matrix[0].length == 0)
return false;
int i = 0;
while(i < matrix.length){
int j = 0;
while(j < matrix[0].length){
if(matrix[i][j] == target)
return true;
else if(matrix[i][j] > target)
break;
j++;
}
i++;
}
return false;
}
}
方法2:二叉搜索树
分析
代码:
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
// 二叉树搜索
if(matrix == null || matrix.length==0 || matrix[0].length ==0 )
return false;
int i = 0,j = matrix[0].length-1;
while(i < matrix.length && j >= 0){
if(matrix[i][j] > target) j--;
else if(matrix[i][j] < target) i++;
else return true;
}
return false;
}
}
offer11 旋转数组的最小值
需求
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为 1。
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
示例
输入:numbers = [3,4,5,1,2] 输出:1
输入:numbers = [2,2,2,0,1] 输出:0
方法1:for循环(倒叙查找)
分析
由于原始数组从小到大排序,之后又进行前面一部分内容旋转到后面,整体会出现先增大 后分界 然后 再增大的过程。
从后向前找或者从前向后找 不是递增的情况 输出即可。
代码
class Solution {
public int minArray(int[] numbers) {
int n = numbers.length;
if(n == 1)
return numbers[0];
for(int i = n-1; i >0 ; i--){
if(numbers[i-1] > numbers[i])
return numbers[i];
}
return numbers[0];
}
}
方法2:二分法
图示
代码
class Solution {
public int minArray(int[] numbers) {
// 二分法
int i = 0 ,j = numbers.length-1;
int m;
while(i < j){
m = (i + j) / 2;
if(numbers[m] > numbers[j])
i = m+1;
else if(numbers[m] < numbers[j])
j = m;
else
j = j-1;
}
return numbers[i];
}
}
offer50 第一次只出现一次的字符
总结
存储出现个数、位置索引。
需求
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
示例
输入:s = “abaccdeff” 输出:‘b’
输入:s = “” 输出:’ ’
方法1:使用Map(value是元素个数)
class Solution {
public char firstUniqChar(String s) {
// 使用Map集合吧
Map<Character,Integer> map = new HashMap<Character,Integer>();
for(int i = 0 ; i < s.length();i++){
Character key = s.charAt(i);
if(map.containsKey(key)){
Integer value = map.get(key);
map.put(key,value+1);
}else{
map.put(key,1);
}
}
// 遍历找value = 1
for(int i = 0; i < s.length() ; i++){
Integer value = map.get(s.charAt(i));
if(value == 1)
return s.charAt(i);
}
return ' ';
}
}
方法2:indexOf 和 lastIndexOf 方法
class Solution {
public char firstUniqChar(String s) {
// 使用indexOf方法 和 lastIndexOf方法
for(int i = 0 ; i < s.length() ; i++){
if(s.indexOf(s.charAt(i)) == s.lastIndexOf(s.charAt(i)))
return s.charAt(i);
}
return ' ';
}
}
方法3:使用Map(value是元素索引)
class Solution {
public char firstUniqChar(String s) {
// 使用Map存储存储索引,如果有重复值为-1,没有重复值为下标
Map<Character,Integer> map = new HashMap<Character,Integer>();
for(int i = 0; i < s.length(); i++){
Integer value = map.get(s.charAt(i));
if(map.containsKey(s.charAt(i)))
map.put(s.charAt(i),-1);
else
map.put(s.charAt(i),i);
}
// 循环遍历
for(int i = 0 ; i < s.length() ; i++){
if(map.get(s.charAt(i)) != -1)
return s.charAt(i);
}
return ' ';
}
}
方法4:使用数组存储个数
class Solution {
public char firstUniqChar(String s) {
// 字符串 转字符数组 用另一个数组存储元素出现的位置,
char[] arr = s.toCharArray();
// 存储26个字母在字符串中出现的次数。
int[] arrint = new int[26];
int index;
for(int i = 0 ; i < arr.length;i++){
index = arr[i] - 'a';
arrint[index]++;
}
// 遍历arrint数组
for(int i = 0 ; i < arr.length;i++){
if(arrint[arr[i]-'a'] == 1)
return arr[i];
}
return ' ';
}
}
方法5:LinkedHashMap(有序的map)
class Solution {
public char firstUniqChar(String s) {
// 使用具有排序功能的LinkedHashMap
Map<Character,Boolean> map = new LinkedHashMap<Character,Boolean>();
char[] arr = s.toCharArray();
// 存储map
for(char ch:arr){
if(map.containsKey(ch))
map.put(ch,!map.containsKey(ch));
// map的值存储的value 表示该键是否是唯一的。有重复=false 没有重复是ture
else
map.put(ch,true);
}
// 遍历map
for(Map.Entry<Character,Boolean> mapch :map.entrySet()){
if(mapch.getValue()) return mapch.getKey();
}
return ' ';
}
}
两数之和 初级算法
需求
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那两个整数,并返回它们的数组下标。
假设每种输入只会对应一个答案 = target的两数之和只有一个解,且每组输入都有解。
数组中同一个元素在答案里不能重复出现 = 结果返回的数组下标不能重复。
解题思路
假设所需的整数数组是sums、整数目标值是target
根据题目内容,每种输入都有唯一对应的答案,也就是说只要输入的值就有解,就会存在一种特殊的解,输入数组只有两个元素,则这两个元素就是它的解。所以需要在每个方法中都需要添加如下代码:
int[] result = {0,1}
if(nums.length <= 2)
{
return result;
}
冒泡排序:使用的是两层循环
for(int i = 0 ; i < sums.length ; i ++)
for( int j = i+1 ; j>= i ; j++)
哈希表的形式(没有提前将数组的值存储在哈希表中)
定义哈希表 Map<Integer,Integer> map = new HashMap<Integer,Integer>() 其中键存储的是数组元素,值存储的是数组的下标;
遍历数组i,获取每一个元素对应的target-sums[i]的值,判断与哈希表中的元素是否一致,map.containsKey(target-sums[i])
一致,返回值是new int[] {map.get(target-sums[i]) ,i};不一致,不执行任何操作
最终如果在if中没有返回值,则return new int[] {};
二分法,在冒泡排序的基础之上,内层循环使用两个指针
外层循环for(int i = 0 ; i < sums.length ; i++)
内存循环for(int j = i+1 , k = sums.length-1 ; j<= k ; j++,k–)
四分法,在二分法的基础上,外层循环也采用两个指针
外层循环for(int i = 0 ,j = sums.length-1 ; i <= j ; i++,j–)
内层循环for(int a = i+1,b=j-1; a <= b ; a++,b–)
二分法 + 哈希表
定义哈希表 Map<Integer,Integer> map = new HashMap<Integer,Integer>(); key=数组的值,value=值对应的下标
循环遍历数组,使用两个指针,for(int i = 0,j = nums.length-1 ; i <= j ; i++,j–)
比较target-nums[i] 或者 target-nums[j] 的值是否在哈希表中。 map.containsKey(target-nums[i]) 或者 map.containsKey(target-nums[j])
存在就给result数组赋值。不存在就把数组元素存储在哈希表中 map.put(nums[i],i) 或者 map.put(nums[j],j)
代码细节
main函数
public static void main(String[] args) {
int[] array = {2,7,11,15};
int target = 9;
// int[] array = {3, 2, 4};
// int[] array = {3,3};
System.out.println("冒泡排序");
method1(array, target);
System.out.println();
System.out.println("哈希表");
int[] result2 = method2(array, target);
System.out.println("[" + result2[0] + "," + result2[1] + "]");
System.out.println("二分法检索");
int[] result3 = method3(array, target);
System.out.println("[" + result3[0] + "," + result3[1] + "]");
System.out.println("四分法检索");
int[] result4 = method4(array, target);
System.out.println("[" + result4[0] + "," + result4[1] + "]");
System.out.println("二分法 + 哈希表");
int[] result5 = method4(array, target);
System.out.println("[" + result5[0] + "," + result5[1] + "]");
}
冒泡排序
public static void method1(int[] array, int target) {
for (int i = 0; i < array.length; i++) {
for (int j = i + 1; j < array.length; j++) {
if (array[i] + array[j] == target) {
System.out.print("[" + i + "," + j + "]");
}
}
}
}
//分析,时间复杂度为O(N^2),空间复杂度为O(1)
2022/6/3
public int[] twoSum(int[] nums, int target) {
// 边界条件 只有两个元素
if(nums.length == 2)
return new int[]{0,1};
//双层for循环
for(int i = 0 ; i < nums.length-1 ; i++){
for(int j = i+1 ;j < nums.length ; j++){
if(nums[j] + nums[i] == target){
return new int[]{i,j};
}
}
}
return new int[]{};
}
哈希表 containsKey、key=数组值、value=值下标
public static int[] method2(int[] array, int target) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < array.length; i++) {
if (map.containsKey(target - array[i])) {
return new int[]{map.get(target - array[i]), i};
}
map.put(array[i], i);
}
return new int[]{};
}
//分析:使用哈希查找时间复杂度是O(1),在加上单层for遍历N个变量,则整体时间复杂度是O(N)
时间是:1ms,内存消耗是:41.4M
二分法,主要用在第二层循环的比较
public static int[] method3(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
for (int j = i, k = nums.length - 1; j <= k; j++, k--) {
if ((target - nums[i]) == nums[j]) {
return new int[]{i,j};
} else if ((target - nums[i]) == nums[k]) {
return new int[]{i,k};
}
}
}
return new int[]{};
}
//分析: 整体的时间复杂度是O(nlogn)
时间:37ms,内存消耗:41.8M
四分法(数组遍历二分 + 找target之和遍历再二分)
public static int[] method4(int[] nums, int target) {
for (int i = 0, j = nums.length - 1; i <= j; i++, j--) {
if(nums[i] + nums[j] == target){
return new int[]{i,j};
}
for (int a = i + 1, b = j - 1; a <= b; a++, b--) {
if (nums[i] + nums[a] == target) {
return new int[]{i,a};
} else if (nums[j] + nums[a] == target) {
return new int[]{a,j};
} else if (nums[i] + nums[b] == target) {
return new int[]{i,b};
} else if (nums[j] + nums[b] == target) {
return new int[]{b,j};
}
}
}
return new int[]{};
}
// 分析:时间复杂度是O(n/2logn)
时间:0ms,内存消耗:41.4M
备注2022/6/3
第二层for循环的两个变量的判断的条件得是 a <= b
否则对于奇数数组的话,最中间的数据没有办法比较了;
比如:{3,2,4},i = 0 j = 2 此时a = 1 b = 1 如果没有 a <= b这个条件,而是 a < b 的情况,这样中间的值i = 1 就没有办法比较了;
二分法 + 哈希表
public int[] twoSum(int[] nums, int target) {
HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
for(int i = 0,j = nums.length-1; i <= j; i++,j--){
if(map.containsKey(target-nums[i])){
return new int[]{map.get(target-nums[i]),i};
}else if(map.containsKey(target-nums[j])){
return new int[]{map.get(target-nums[j]),j};
}else if(nums[i] + nums[j] == target){
return new int[]{i,j};
}
map.put(nums[i],i);
map.put(nums[j],j);
}
return new int[]{};
}
时间:0ms,内存损耗:41.5M。
注意 |
---|
2022/5/6二分法要注意对于端点的思考。也就是 i 和 j 之和等于target的情况,因为在第二层遍历的时候,便没有进行两者的比较 |
2022/6/3 注意数组奇数个数的情况,循环的条件得是 i <= j 而不是 i < j |
删除数组中的重复项
需求
一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
元素的 相对顺序 应该保持 一致 。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。
将最终结果插入 nums 的前 k 个位置后返回 k 。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
思路
双指针
使用双指针的形式
一个指针表示可以被覆盖的位置slow,另一个表示比较元素fast,两个指针的初始值都是从1开始的。
当nums[slow-1] != nums[fast] 的时候,说明标号0和标号1的内容不一样,所以执行nums[slow] = nums[fast] 的赋值操作,并执行slow++、fast++
内容相同,则将fast++
代码
双指针
方式1:
public static int removeDuplicates(int[] nums) {
// 指针i表示可以被覆盖的位置,
// 指针j表示后续查找元素
int n = nums.length;
if(n == 0){
return 0;
}
int fast = 1,slow = 1;
while(fast<n) {
if (nums[fast] != nums[slow - 1]) {
nums[slow] = nums[fast];
++slow;
}
++fast;
}
return slow;
}
备注2022/6/3
注意两个指针是从1的索引开始的,而不是slow = 0,fast = 1;
方式2:使用了for循环,但是在leecode系统中提交没有问题,但是自己电脑运行有问题。
输出结果正确是:5
但是实际输出结果是:8
// idea_face - arrayAndMatrix
public static int removeDuplicates2(int[] nums) {
if(nums.length == 0 || nums == null){
return 0;
}
int left = 0;
for(int right = 1; right <nums.length;right++){
if(nums[left] != nums[right]){
nums[++left] = nums[right];
}
System.out.println(left + "," + right);
}
return ++left;
}
方法3:
public int removeDuplicates(int[] nums) {
// 定义待覆盖元素位置
int t = 0;
for(int i = 0 ; i <nums.length ; i++){
if( i == 0 || nums[i] != nums[i-1] ) nums[t++] = nums[i];
}
return t;
}