目录
查找算法
1 二维数组的查找
剑指 Offer 04. 二维数组中的查找https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/
1. 暴力遍历
2. 线性+二分遍历
- 先判断 每一维度第一个是否小于target
- 小于target 进一步判断 最后一个与target
- 最后一个小于target 直接continue
- 等于 返回true
- 大于 二分查找 确定是否有
- 等于 返回true
- 大于 结束循环
- 小于target 进一步判断 最后一个与target
开始前要先判断 matrix.length ==0 和matrix[0].length == 0的情况直接返回false
package jzof.Day05;
/**
* @author ahan
* @create_time 2021-11-08-1:54 下午
*/
public class _04 {
public static void main(String[] args) {
int[][] matrix = { {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} };
System.out.println(new _04().findNumberIn2DArray(matrix, 5));
// System.out.println(new _04().binaySearch(matrix[0], 5));
// System.out.println(new _04().binaySearch(matrix[1], 5));
}
public boolean findNumberIn2DArray(int[][] matrix, int target) {
boolean t = false;
if (matrix.length == 0){
return false;
}
for (int i = 0; i < matrix.length; i++) {
if(matrix[i][0] < target){
if(matrix[i][matrix[0].length - 1] == target){
t = true;
break;
} else if (matrix[i][matrix[0].length - 1] > target){
t = binaySearch(matrix[i], target);
if (t){
break;
}
} else{
continue;
}
}else if(matrix[i][0] == target){
t = true;
}else{
break;
}
}
return t;
}
public boolean binaySearch(int [] nums, int target){
int start = 0, end = nums.length - 1;
int mid = 0;
boolean temp = false;
while(start <= end){
mid = ((end - start) >> 1) + start;
if(target == nums[mid]){
temp = true;
}
if(target > nums[mid]){
start = mid + 1;
}else{
end = mid - 1;
}
}
return temp;
}
}
复杂度分析
- 时间复杂度:O(n*log(m))。访问到的下标的行最多 n 次,行内二分查找 ,因此循环体最多执行 n*log(m) 次。
- 空间复杂度:O(1)
3. 转换成树
若使用暴力法遍历矩阵
matrix
,则时间复杂度为 O(NM)O(NM) 。暴力法未利用矩阵 “从上到下递增、从左到右递增” 的特点,显然不是最优解法。
如下图所示,将矩阵逆时针旋转 45° ,并将其转化为图形式,发现其类似于 二叉搜索树 ,即对于每个元素,其左分支元素更小、右分支元素更大。因此,通过从 “根节点” 开始搜索,遇到比 target 大的元素就向左,反之向右,即可找到目标值 target 。
“根节点” 对应的是矩阵的 “左下角” 和 “右上角” 元素,本文称之为 标志数 ,以 matrix 中的 左下角元素 为标志数 flag ,则有:
若 flag > target ,则 target 一定在 flag 所在 行的上方 ,即 flag 所在行可被消去。
若 flag < target ,则 target 一定在 flag 所在 列的右方 ,即 flag 所在列可被消去。
复杂度分析:
- 时间复杂度 O(M+N) :其中,N 和 M 分别为矩阵行数和列数,此算法最多循环 M+N 次。
- 空间复杂度 O(1) : i, j 指针使用常数大小额外空间。
此题解来自作者:jyd 太强了~
public boolean findNumberIn2DArray_1(int[][] matrix, int target) {
int i = matrix.length - 1, 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;
}
2 旋转数组的最小数字
剑指 Offer 11. 旋转数组的最小数字https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/
1. 遍历比较
遇到后一个比前一个小的,直接返回结果。
public int minArray_1(int[] numbers) {
int temp = 0;
for (int i = 0; i < numbers.length - 1; i++) {
if (numbers[i+1] < numbers[i]){
return numbers[i+1];
}
}
return numbers[0];
}
2. 循环遍历
3 第一个只出现一次的字符
剑指 Offer 50. 第一个只出现一次的字符https://leetcode-cn.com/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof/
1. 两个set 求差
一开始想到hashmap存 或者 俩set 一个一次的 一个多次的,考虑到第一个,在存的时候可以给key上做点“手脚”
首先是一个hashset存出现一次的一个存出现多次的,最后再利用一个hashset存一个差集,然后遍历一遍,找出第一个在差集内的字母。
在LeetCode上第一次写题解QAQ
public char firstUniqChar(String s) {
Set<Character> set_1 = new HashSet<>();
Set<Character> set_2 = new HashSet<>();
Set<Character> set_sub = new HashSet<>();
for (int i = 0; i < s.length(); i++) {
char t = s.charAt(i);
if(!set_1.add(t)){
set_2.add(t);
}
}
set_sub.addAll(set_1);
set_sub.removeAll(set_2);
for (int i = 0; i < s.length(); i++) {
if(set_sub.contains(s.charAt(i))){
return s.charAt(i);
}
}
return ' ';
}
2.1 使用哈希表存储频数
public char firstUniqChar_1(String s) {
HashMap<Character, Integer> map = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
if(map.get(s.charAt(i)) != null)
map.put(s.charAt(i), map.get(s.charAt(i)) + 1);
else{
map.put(s.charAt(i), 1);
}
}
for (int i = 0; i < s.length(); i++) {
if(map.get(s.charAt(i)) != null && map.get(s.charAt(i)) == 1){
return s.charAt(i);
}
}
return ' ';
}
比set效果差挺多,map详细存了每个字母的出现次数。
复杂度分析
- 时间复杂度:O(n),其中 n 是字符串 s 的长度。需要进行两次遍历。
- 空间复杂度:O(∣Σ∣),其中 Σ 是字符集,在本题中 s 只包含小写字母,因此 ∣Σ∣≤26。需要 O(∣Σ∣) 的空间存储哈希映射。
if(map.get(s.charAt(i)) != null)
map.put(s.charAt(i), map.get(s.charAt(i)) + 1);
else
map.put(s.charAt(i), 1);
等价于
map.put(s.charAt(i), map.getOrDefault(s.charAt(i), 0) + 1);
效率上改进了一丢丢~
2.2 使用哈希表存储索引
可以对方法一进行修改,使得第二次遍历的对象从字符串变为哈希映射。
具体地,对于哈希映射中的每一个键值对,键表示一个字符,值表示它的首次出现的索引(如果该字符只出现一次)或者 -1(如果该字符出现多次)。当第一次遍历字符串时,设当前遍历到的字符为 c,如果 c 不在哈希映射中,就将 c 与它的索引作为一个键值对加入哈希映射中,否则将 c 在哈希映射中对应的值修改为 -1。
在第一次遍历结束后,只需要再遍历一次哈希映射中的所有值,找出其中不为 −1 的最小值,即为第一个不重复字符的索引,然后返回该索引对应的字符。如果哈希映射中的所有值均为 -1,就返回空格。
public char firstUniqChar_1_1(String s) {
HashMap<Character, Integer> map = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
if(map.get(s.charAt(i)) == null)
map.put(s.charAt(i), i);
else{
map.put(s.charAt(i), -1);
}
}
int first = s.length();
for (Map.Entry<Character, Integer> entry : map.entrySet()) {
int pos = entry.getValue();
if (pos != -1 && pos < first) {
first = pos;
}
}
return first == s.length() ? ' ' : s.charAt(first);
}
值得记录下来的是记录index的想法和map.entrySet()的遍历方式
2.2 LinkedHashMap 解法
遍历可以用keyset()+get()遍历,也可以用map.entrySet()+entry.getValue()、getKey()遍历
class Solution {
public char firstUniqChar(String s) {
HashMap<Character, Integer> map = new LinkedHashMap<>();
for (int i = 0; i < s.length(); i++) {
// if(map.get(s.charAt(i)) != null)
// map.put(s.charAt(i), map.get(s.charAt(i)) + 1);
// else{
// map.put(s.charAt(i), 1);
// }
map.put(s.charAt(i), map.getOrDefault(s.charAt(i), 0) + 1);
}
// for (Map.Entry<Character, Integer> entry : map.entrySet()) {
// if (entry.getValue() == 1) {
// return entry.getKey();
// }
// }
for(Character c : map.keySet()){
if(map.get(c) == 1){
return c;
}
}
return ' ';
}
}
效率一般
2.3 存Boolean状态 (很强)
public char findUniqChar_1_3(String s){
HashMap<Character, Boolean> dic = new HashMap<>();
char[] sc = s.toCharArray();
for(char c : sc)
dic.put(c, !dic.containsKey(c));
for(char c : sc)
if(dic.get(c)) return c;
return ' ';
}
3. 利用数组
题设说都是小写字母,就可以利用小写字母的Unicode码范围直接在数组上进行记录~
public char firstUniqChar_2(String s) {
int[] nums = new int[27];
for (int i = 0; i < s.length(); i++) {
nums[(s.charAt(i) - 97)]++;
}
for (int i = 0; i < s.length(); i++) {
if(nums[(s.charAt(i) - 97)] == 1){
return s.charAt(i);
}
}
return ' ';
}
4. 利用队列存储
也可以借助队列找到第一个不重复的字符。队列具有「先进先出」的性质,因此很适合用来找出第一个满足某个条件的元素。
具体地,我们使用与方法二相同的哈希映射,并且使用一个额外的队列,按照顺序存储每一个字符以及它们第一次出现的位置。当我们对字符串进行遍历时,设当前遍历到的字符为 cc,如果 cc 不在哈希映射中,我们就将 cc 与它的索引作为一个二元组放入队尾,否则我们就需要检查队列中的元素是否都满足「只出现一次」的要求,即我们不断地根据哈希映射中存储的值(是否为 -1−1)选择弹出队首的元素,直到队首元素「真的」只出现了一次或者队列为空。
在遍历完成后,如果队列为空,说明没有不重复的字符,返回空格,否则队首的元素即为第一个不重复的字符以及其索引的二元组。
class Solution {
public char firstUniqChar(String s) {
Map<Character, Integer> position = new HashMap<Character, Integer>();
Queue<Pair> queue = new LinkedList<Pair>();
int n = s.length();
for (int i = 0; i < n; ++i) {
char ch = s.charAt(i);
if (!position.containsKey(ch)) {
position.put(ch, i);
queue.offer(new Pair(ch, i));
} else {
position.put(ch, -1);
while (!queue.isEmpty() && position.get(queue.peek().ch) == -1) {
queue.poll();
}
}
}
return queue.isEmpty() ? ' ' : queue.poll().ch;
}
class Pair {
char ch;
int pos;
Pair(char ch, int pos) {
this.ch = ch;
this.pos = pos;
}
}
}
在维护队列时,我们使用了「延迟删除」这一技巧。也就是说,即使队列中有一些字符出现了超过一次,但它只要不位于队首,那么就不会对答案造成影响,我们也就可以不用去删除它。只有当它前面的所有字符被移出队列,它成为队首时,我们才需要将它移除。