阿翰 剑指offer 之 Day05 查找算法

目录

查找算法

1 二维数组的查找

1. 暴力遍历

2. 线性+二分遍历

3. 转换成树

2 旋转数组的最小数字 

1. 遍历比较

2. 循环遍历

3 第一个只出现一次的字符

1. 两个set 求差

2.1 使用哈希表存储频数

2.2 使用哈希表存储索引

2.2 LinkedHashMap 解法

2.3 存Boolean状态 (很强)

3. 利用数组 

4. 利用队列存储


查找算法

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
    • 大于 结束循环

       开始前要先判断 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;
        }
    }
}
 

在维护队列时,我们使用了「延迟删除」这一技巧。也就是说,即使队列中有一些字符出现了超过一次,但它只要不位于队首,那么就不会对答案造成影响,我们也就可以不用去删除它。只有当它前面的所有字符被移出队列,它成为队首时,我们才需要将它移除。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值