剑指Offer-哈希表部分

数组中重复的数字

找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:23 

解题思路1

哈希表

var findRepeatNumber = function (nums) {
        let map = new Map();
        for(var v of nums){
            if(!map.has(v)){
                map.set(v,1)
            }else{
                return v
            }
        }
        return null
};

时间复杂度O(n),空间复杂度O(n)

解题思路2

原地交换
从题目描述可以知道,所有数字都在 0 ~ n-1 的范围内。因此不需要额外开辟空间,每次遍历时,检查当前元素是否放在了正确位置上(例如元素 i 应该放在下标为 i 的位置上)。如果放在了正确位置上,那么继续循环。否则:
下标为 num 的元素 == num,说明当前元素 num 是重复的,直接返回。
下标为 num 的元素 != num,交换当前元素和下标为 num 的元素,将当前元素放入到正确位置上。

var findRepeatNumber = function(nums) {
    const length = nums.length;
    for (let i = 0; i < length; ++i) {
        while ((num = nums[i]) !== i) {
            if (num === nums[num]) {
                return num;
            }
            [nums[i], nums[num]] = [nums[num], nums[i]];
        }
    }
};

时间复杂度O(n),空间复杂度O(1)

最长不含重复字符的子字符串

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

示例 1:

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

解题思路1

滑动窗口
准备 2 个指针 i、j,i 指向窗口左边,j 指向右边。指针每次可以向前“滑动”一个位置,它们之间的区域就是“窗口”。
整体流程如下:
1.准备哈希表 map。key 是 char,value 是 boolean,代表字符 char 是否出现在滑动窗口内
2.i 和 j 初始化为 0,结果 max初始化为 0
3.检查s[j]是否出现过:
没有出现过,扩大窗口:记录s[j],指针 j 向右滑动一格,更新 max
出现过,缩小窗口:指针 i 向右移动一格,map[s[i]]更新为 false
4.如果 i 和 j 没有越界,回到 3,否则返回 max

var lengthOfLongestSubstring = function(s) {
   var n = s.length;
   var map = new Map();
   let i=0,j=0;
   let max = 0;
   while(i<n && j<n){
       if(!map.get(s[j])){
           max = Math.max(j-i+1,max);
           map.set(s[j],true);
           j++;
       }else{
           map.set(s[i],false);
           i++;
       }
   }
   return max
};

解题思路2

解题思路1中的i++做了些重复的工作,比如"pwkwelo",i++触发的条件是w重复后,接着从第一个w开始,其实第一个w完全都不需要循环,因为长度肯定不会大于max,所以直接i跳到k处就可以
在解法 1 的流程中的第 3 步,如果s[j]出现在滑动窗口内,采用的做法是左边逐步缩小滑动窗口。事实上,不需要逐步缩小。假设滑动窗口内和s[j]相同字符下标是 j’,那么直接跳过[i, j’] 范围即可。
为了做到“跳动优化”,需要改造一下对哈希表 map 的用法:key 还是 char;value 变为 int,记录 char 对应的下标。

var lengthOfLongestSubstring = function(s) {
    const length = s.length;
    const map = new Map();
    let i = 0,
        j = 0;
    let ans = 0;
    while (i < length && j < length) {
        // 容易理解:检查s[j]是否出现过,并且s[j]重复的字符是否在当前的滑动窗口中
        if (map.has(s[j]) && map.get(s[j]) >= i) {
            i = map.get(s[j]) + 1;
        }
        ans = Math.max(j - i + 1, ans);
        map.set(s[j], j);
        ++j;
    }
    return ans;
};

第一个只出现一次的字符

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例:
s = "abaccdeff"
返回 "b"
s = "" 
返回 " "

参考作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof/solution/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-by-3zqv5/
来源:力扣(LeetCode)

解题思路1

使用哈希表存储频数;
我们可以对字符串进行两次遍历。
在第一次遍历时,我们使用哈希映射统计出字符串中每个字符出现的次数。在第二次遍历时,我们只要遍历到了一个只出现一次的字符,那么就返回该字符,否则在遍历结束后返回空格.

var firstUniqChar = function(s) {
    const frequency = _.countBy(s); 
    //若s为‘leetcode’ _.countBy 返回为{"l":1,"e":3,"t":1,"c":1,"o":1,"d":1}
    //Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
    //Array.entries()方法返回一个新的Array Iterator对象,该对象包含数组中每个索引的键/值对
    for (const [i, ch] of Array.from(s).entries()) {
        if (frequency[ch] === 1) {
            return ch;
        }
    }
    return ' ';
};

复杂度分析
时间复杂度:O(n),其中 n 是字符串 s 的长度。我们需要进行两次遍历。
空间复杂度:O(∣Σ∣),其中Σ 是字符集,在本题中 s 只包含小写字母,因此 ∣Σ∣≤26。我们需要 O(∣Σ∣) 的空间存储哈希映射。

解题思路2

使用哈希表存储索引:
我们可以对方法一进行修改,使得第二次遍历的对象从字符串变为哈希映射。
具体地,对于哈希映射中的每一个键值对,键表示一个字符,值表示它的首次出现的索引(如果该字符只出现一次)或者 -1−1(如果该字符出现多次)。当我们第一次遍历字符串时,设当前遍历到的字符为 cc,如果 cc 不在哈希映射中,我们就将 cc 与它的索引作为一个键值对加入哈希映射中,否则我们将 cc 在哈希映射中对应的值修改为 -1−1。
在第一次遍历结束后,我们只需要再遍历一次哈希映射中的所有值,找出其中不为 -1−1 的最小值,即为第一个不重复字符的索引,然后返回该索引对应的字符。如果哈希映射中的所有值均为 -1−1,我们就返回空格.

var firstUniqChar = function(s) {
    const position = new Map();
    const n = s.length;
    for (let [i, ch] of Array.from(s).entries()) {
        if (position.has(ch)) {
            position.set(ch, -1);
        } else {
            position.set(ch, i);
        }
    }
    let first = n;
    for (let pos of position.values()) {
        if (pos !== -1 && pos < first) {
            first = pos;
        }
    }
    return first == n ? ' ' : s[first];
};

复杂度分析
时间复杂度:O(n),其中 n 是字符串 s 的长度。第一次遍历字符串的时间复杂度为 O(n),第二次遍历哈希映射的时间复杂度为O(∣Σ∣),由于 s 包含的字符种类数一定小于 s 的长度,因此 O(∣Σ∣) 在渐进意义下小于O(n),可以忽略。
空间复杂度:O(∣Σ∣),其中 Σ 是字符集,在本题中 s 只包含小写字母,因此 Σ∣≤26。我们需要 O(∣Σ∣) 的空间存储哈希映射。

解题思路3

队列:
我们也可以借助队列找到第一个不重复的字符。队列具有「先进先出」的性质,因此很适合用来找出第一个满足某个条件的元素。

具体地,我们使用与方法二相同的哈希映射,并且使用一个额外的队列,按照顺序存储每一个字符以及它们第一次出现的位置。当我们对字符串进行遍历时,设当前遍历到的字符为 c,如果 c 不在哈希映射中,我们就将 c 与它的索引作为一个二元组放入队尾,否则我们就需要检查队列中的元素是否都满足「只出现一次」的要求,即我们不断地根据哈希映射中存储的值(是否为 -1)选择弹出队首的元素,直到队首元素「真的」只出现了一次或者队列为空。
在遍历完成后,如果队列为空,说明没有不重复的字符,返回空格,否则队首的元素即为第一个不重复的字符以及其索引的二元组。
小贴士
在维护队列时,我们使用了「延迟删除」这一技巧。也就是说,即使队列中有一些字符出现了超过一次,但它只要不位于队首,那么就不会对答案造成影响,我们也就可以不用去删除它。只有当它前面的所有字符被移出队列,它成为队首时,我们才需要将它移除。

var firstUniqChar = function(s) {
    const position = new Map();
    const q = [];
    const n = s.length;
    for (let [i, ch] of Array.from(s).entries()) {
        if (!position.has(ch)) {
            position.set(ch, i);
            q.push([s[i], i]);
        } else {
            position.set(ch, -1);
            while (q.length && position.get(q[0][0]) === -1) {
                q.shift();
            }
        }
    }
    return q.length ? q[0][0] : ' ';
};
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值