【leetcode刷题笔记】剑指:哈希表标签

leetcode刷题笔记(剑指:哈希表标签)

哈希只是种数据结构,算不上是什么思想算法应该,这里只是为了熟悉哈希的用法,要想起这个东西。

面试题 01.01 判定字符是否唯一(easy)

实现一个算法,确定一个字符串 s 的所有字符是否全都不同。

首先很容易想到使用哈希表的方法,此时时间和空间复杂度都为O(n)

class Solution {
public:
    bool isUnique(string s) {
        int n=s.size();
        unordered_map<char,int> umap;
        for(int i=0;i<n;++i){
            if(umap[s[i]]) return false;
            umap[s[i]]++;
        }
        return true;
    }
};

采用哈希表是为了记录每个字符出现的次数,但会使用多余的空间。因此可以采用位运算,每一个bit位记录一个字符是否出现,从而不使用额外的数据结构。

class Solution {
public:
    bool isUnique(string s) {
        int n=s.size(),tmp=0,step;
        for(int i=0;i<n;++i){
            step=s[i]-'a';
            if(tmp & 1<<step) return false;
            tmp=tmp|(1<<step);
        }
        return true;
    }
};

剑指 Offer 03:数组中重复的数字(easy)

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

和上一题基本上一样。首先想到哈希表。但本题不能采用位运算,因为上一题只有小写字母26个,而本题数据范围为2-10000,10000个bit位,会越界?。

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int n=nums.size();
        unordered_map<int,int> umap;
        for(int i=0;i<n;++i){
            if(umap[nums[i]]) return nums[i];
            umap[nums[i]]++;
        }
        return -1;
    }
};

注意题目的条件,他并不是任意的随机一个数组,而是长度为n,且范围在0~n-1。因此可以利用数组的特殊性进行解题优化。

如果没有重复或者缺失,每个元素都应该满足nums[i]=i,每个元素都有它应该在的一个位置。因此利用这个性质,可以依次遍历,把每个元素放在正确的位置上,如果在交换过程中,待交换位置元素已经放置正确,那说明交换位置的nums[i],和待交换位置的nums[nums[i]]相同,因此其为重复元素。

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int n=nums.size();
        for(int i=0;i<n;++i){
            while(nums[i]!=i){
                if(nums[i]==nums[nums[i]]) return nums[i];
                //交换位置,当前于元素归位
                int tmp_inx=nums[i];
                nums[i]=nums[tmp_inx];
                nums[tmp_inx]=tmp_inx;
            }
        }
        return -1;
    }
};

剑指 Offer 07:重建二叉树(medium)

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

class Solution {
public:
    unordered_map<int,int> umap;
    int inx;
    TreeNode* helper(vector<int>& preorder, vector<int>& inorder,int left,int right){
        if(left>right) return nullptr;  

        int root_val=preorder[inx];
        TreeNode* node=new TreeNode(root_val);
        int root_idx_in=umap[root_val];
        
        ++inx;
        node->left=helper(preorder,inorder,left,root_idx_in-1);
        node->right=helper(preorder,inorder,root_idx_in+1,right);

        return node;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n=preorder.size();
        inx=0;
        if(n==0) return nullptr;
        for(int i=0;i<n;++i) umap[inorder[i]]=i;    //存储中序遍历的元素对应索引 
        //对于前序遍历,第一个元素是根节点
        //对于中序遍历,根节点的前后子数组分别对应左右子树
        return helper(preorder,inorder,0,n-1);
    }
};

剑指 Offer 39:多数元素(easy)

数组中出现次数超过一半的数字

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

首先找多数元素,肯定可以想到hash计数

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        unordered_map<int,int> umap;
        int n=nums.size();
        for(int i=0;i<n;++i){
            umap[nums[i]]++;
            if(umap[nums[i]]>n/2) return nums[i];
        }
        return -1;
    }
};

考虑题目的特殊性,多数元素大于元素数量的一半。因此可以先排序,中间元素一定是多数元素。

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        //排序,然后中间的
        sort(nums.begin(),nums.end());
        return nums[nums.size()/2];
    }
};

对于本题,还可以采用投票法。将多数元素看为1,其他为-1,最后相加结果一定大于0。

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int sum=0,tmp;
        for(int i=0;i<nums.size();++i){
            if(sum==0){
                sum++;
                tmp=nums[i];
                continue;
            }
            if(nums[i]==tmp) sum++;
            else sum--;
        }
        return tmp;
    }
};

剑指 Offer 35:复杂链表的复制(medium)

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

采用哈希表建立各节点的映射关系。

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/
class Solution {
public:
    Node* copyRandomList(Node* head) {
        if(head == nullptr) return nullptr;
        Node* node=head;
        unordered_map<Node*,Node*> umap;
        while(node!=nullptr){
            umap[node]=new Node(node->val);
            node=node->next;
        }
        node=head;
        while(node!=nullptr){
            umap[node]->next=umap[node->next];
            umap[node]->random=umap[node->random];
            node=node->next;
        }
        return umap[head];
    }
};

剑指 Offer 48:最长不含重复字符的子字符串(medium) ————————记得看其他解法

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

采用unordered_map记录出现的字符,value为字符对应的索引。

采用left记录子字符串的起始索引。当 当前字符s[i]出现过的时候,此时子字符串的长度为i-left,并更新left=umap[s[i]]+1。

此时,需要将left~umap[s[i]]范围内的字符对应value置为0,接着进行下一轮子字符串的遍历查询。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n=s.size();
        //哈希表记录已经出现的字符,key:字符 value:索引
        //如果找到了,则将索引idx及之前的 字符的value记为0
        int ans=0;
        int left=0;
        unordered_map<char,int> umap;
        for(int i=0;i<n;++i){
            if(i==0){
                //i=0的时候,特殊处理,以免与未出现过的字符产生混淆
                umap[s[i]]=n;
            }
            else{
                if(umap[s[i]]==0){
                    umap[s[i]]=i;
                }
                else{
                    ans=max(ans,i-left);
                    int j;
                    if(umap[s[i]]==n) j=0;
                    else j=umap[s[i]];
                    //将出现过字符及之前的字符value置为0
                    for(int k=left;k<=j;++k){
                        umap[s[k]]=0;
                    }
                    umap[s[i]]=i;
                    left=j+1;
                }
            }
        }
        ans=max(ans,n-left);
        return ans;
    }
};

还有其他解法欸,后面再继续

剑指 Offer 49:丑数(medium)

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

标签里加了哈希表,但是感觉用动态规划三指针更好懂。题解里给的哈希表标签是用来建堆时去重的。也去学习下吧!!!不要懒

动态规划做法:下一个丑数是通过前面的丑数*2 *3 *5 然后比较出来的最小的数得到的。每个丑数都需要 *2 *3 *5 ,但是无法保证他的位置。题解里使用了三指针n1,n2,n3分别代表 *2 *3 *5 后的数,而i1,i2,i3是dp数组中的索引,如果乘积后成为了新的丑数或者和新的丑数相等,索引就会+1,由此保证不会有乘积/丑数被遗漏。

class Solution {
public:
    int nthUglyNumber(int n) {
        //首先理解题意,丑数是可以通过只用2 3 5随便怎么乘积可以得到的数
        vector<int>dp(n);
        dp[0]=1;
        int i1=0,i2=0,i3=0;
        for(int i=1;i<n;++i){
            int n1=dp[i1]*2;
            int n2=dp[i2]*3;
            int n3=dp[i3]*5;
            dp[i]=min(n1,min(n2,n3));
            if(dp[i]==n1) i1++;
            if(dp[i]==n2) i2++;
            if(dp[i]==n3) i3++;
        }
        return dp[n-1];
    }
};

最小堆+哈希。关于优先队列,还有堆,其实还不太熟悉。相当于采用堆是把当前丑数都*2 *3 *5 ,然后存入堆中排序,是堆顶的时候取出,有点暴力那味。而动态规划是一个个的按顺序的找。也很明显这个复杂度高很多。

(25条消息) c++优先队列(priority_queue)用法详解_c++优先级队列_吕白_的博客-CSDN博客

class Solution {
public:
    int nthUglyNumber(int n) {
        vector<int> factors = {2, 3, 5};
        unordered_set<long> seen;
        priority_queue<long, vector<long>, greater<long>> heap; //小顶堆,升序排列的
        seen.insert(1L); //1L就是1,但long类型,不然后面会溢出
        heap.push(1L);
        int ugly = 0;
        for (int i = 0; i < n; i++) {
            long curr = heap.top();
            heap.pop();				//堆顶最小,弹出记录
            ugly = (int)curr;		//第i+1个丑数
            for (int factor : factors) {
                long next = curr * factor;
                //没有出现过,所以插入堆和set中保存,priority_queue会自动排序,本质是个堆实现
                if (!seen.count(next)) {	
                    seen.insert(next);
                    heap.push(next);
                }
            }
        }
        return ugly;
    }
};

//作者:LeetCode-Solution
//链接:https://leetcode.cn/problems/chou-shu-lcof/solution/chou-shu-by-leetcode-solution-0e5i/
//来源:力扣(LeetCode)
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 50:第一个只出现一次的字符(easy) ————————队列后面记得看

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

很快就能想到hash表计数啦。时间复杂度是O(n)

class Solution {
public:
    char firstUniqChar(string s) {
        int n=s.size();
        unordered_map<char,int> umap;
        for(int i=0;i<n;++i){
            umap[s[i]]++;
        }
        for(int i=0;i<n;++i){
            if(umap[s[i]]==1) return s[i];
        }
        return ' ';
    }
};

题解里还提到了队列,平时做题基本上想不到用队列/栈之类的。


剑指 Offer 52:两个链表的第一个公共节点(easy)

输入两个链表,找出它们的第一个公共节点。

最开始能想到的,哈希表先存储一条链的节点,再遍历第二条,如果有一样的就是公共节点。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        //哈希表先存储一条链的节点,再遍历第二条,如果有一样的就是公共节点
        ListNode *node1=headA,*node2=headB;
        unordered_map<ListNode*,int> umap;
        while(node1!=nullptr){
            umap[node1]++;
            node1=node1->next;
        }
        while(node2!=nullptr){
            if(umap[node2]) return node2;
            node2=node2->next;
        }
        return nullptr;
    }
};

采用哈希会用到额外的空间,可以考虑双指针的方法。这个也好理解,反向来推的话,比如两个链表长度分别为len1,len2,两个指针node1,node2分别从两条链表起始开始,走到头后又从另一条链表开始,那么他们都走过的长度是len1+len2,相交节点到末尾距离令为tmp,是他们最后都为经历的,因此他们都会在len1+len2-tmp步长的时候相遇。再正向去想······感觉有点没解释清楚。

评论区真的——浪漫的嘞。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode *node1=headA,*node2=headB;
        while(node1!=node2){
            if(node1!=nullptr) node1=node1->next;
            else node1=headB;
            if(node2!=nullptr) node2=node2->next;
            else node2=headA;
        }
        return node2;  
    }
};

剑指 Offer 53-2:0~n-1中缺失的数字(easy)

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

给了用哈希表的标签,官方题解就是把nums中元素放到set里面,然后遍历0~n-1看缺哪个。。只能说很抽象没必要吧。还不如直接遍历。

第一反应就是遍历数组,因为已经有序,如果不缺的话nums[i]=i,如果不满足那么缺的就是i呀。然后考虑边界情况。这个应该算暴力法吧。

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        //已经有序
        int n=nums.size();
        for(int i=0;i<n;++i){
            if(nums[i]!=i) return i;
        }
        return n;
    }
};

上面这个方法时间复杂度O(n)。因为已经有序,还可以考虑二分,降低复杂度。二分一个很重要的要判断边界问题,什么时候去等什么时候不取等!!!

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        //采用二分实际上还是对nums[i]==i进行判断,只是不是全部遍历罢了
        int left=0,right=nums.size()-1;
        while(left<right){
            int mid=(right-left)/2+left;
            if(nums[mid]==mid) left=mid+1;  //相等则一定不缺!!
            else right=mid; 				//否则存在缺的可能
        }
        //考虑右边界
        return nums[right] != right ? right : right + 1;
    }
};

数学方法,就是0~n-1的和可求,原数组和也可求,两个相减就是缺的数,时间复杂度也为O(n)。没必要写。

位运算,两个一样的数异或为0,异或具有交换组合性质。因此可以将数组的数相异或,再和0~n-1异或。最后剩的就是差的那个。

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int n=nums.size();
        int ans=0;
        for(int i=0;i<n;++i){
            ans^=nums[i];
        }
        for(int i=0;i<=n;++i){	//这里需要注意的是n-1的递增序列,它的可能最大数为n!!因此这里要取等
            ans^=i;
        }
        return ans;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值