LeetCode+ 1 - 5 哈希表|数学|哈希表、字符串、滑动窗口|递归|枚举

两数之和

算法标签:数组、哈希表

 

给出一个数组和一个目标值,让我们在数组中找到两个不同的数,满足这两个数的和等于目标值,要求返回它们的下标,保证一定有解,不需要考虑无解的情况

如果有多组解,随便输出一组解即可

样例:在 [ 2,7,11,15 ] 中选两个数,使得它们的和是 9,选择 2 和 7 即可,注意返回的是这两个数的下标,因此返回的是 0 和 1

暴力枚举两个数,两重循环枚举下标 i、j,如果和 nums[ i ] + nums[ j ] 等于目标值 target 就返回,时间复杂度为 O(n^2)

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target)
    {
        vector<int> res;
        for (int i = 0; i < nums.size(); i++)
        {
            for (int j = 0; j < i; j++)
            {
                if (nums[i] + nums[j] == target)
                {
                    res = vector<int>({j, i});
                    break;
                }
            }
            if (res.size() > 0) break;
        }
        return res;
    }
};

排序后双指针,但是排序的时间复杂度为 O(nlogn)

下面介绍时间复杂度为 O(n) 的做法:

每次枚举第二个数,在哈希表中找这个数前面有没有一个数,使得这个数加上前面那个数和等于 target

如果这个数是 Si,就相当于问这个数前面有没有某一个数的值是 target - Si,就是查询某一个数有没有存在,使用哈希表

哈希表可以用 O(1) 的时间找出来一个数是否存在

开一个哈希表记录前面所有数,从前往后扫描,每扫描一个数就把这个数放到哈希表里面,当我们扫描到 Si 的时候,相当于把 Si 前面所有的数都放到了哈希表中,在哈希表中查询前面是否有一个数的值为 target - si,如果存在的话把下标找出来

每个数最多往哈希表里面插一次,查询哈希表一次,哈希表时间复杂度为 O(1),因此整个算法的时间复杂度就是 O( n )

c++ unordered_map 判断某个键是否存在

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        //定义哈希表把每个数值映射到它的下标
        unordered_map<int,int> heap;
        //从前往后去遍历一下每个数
        for(int i = 0;i < nums.size();i++) {
            //要查询的数r
            int r = target - nums[i];
            //看查询的值是不是存在 如果存在r,说明已经找到两个数它们的和是target 
            //返回键对应的值也就是下标
            if(heap.count(r)) return {heap[r],i};
            //如果不存在r 就把当前这个数插到哈希表里面
            heap[nums[i]] = i;
        }
        //保证一定有解,前面一定会return 这里是为了避免语法错误
        return{};
    }
};

两数相加

算法标签:递归、链表、数学

 

模拟加法,用一个链表来表示一个数,表示的时候是反着来表示的,先表示个位 8 ,再表示十位 0,最后表示百位 7

题目给出两个用链表表示的数,计算它们的和,并且这个和也要用链表来表示

上面一个数加上下面一个数,这两个数的位数不一定一样

这个链表是从右往左串的,先看两个链表的第一个数,两个链表的第一个数相加除以10 的余数(只保留个位)就是和的第一个数(个位)

进位用 t 来表示,可能是 0(如果小于 10),可能是 1(如果大于等于 10)

从下一位开始是第一个链表的值加上第二个链表的值,再加上进位,最终存储的是这三个数的和的个位(也就是除以 10 的余数),如果和大于等于 10 要往前进一位,否则不需要进位

从个位开始循环,循环的时候看一下:如果某一个链表没有循环完就要一直做,如果有进位,并且进位不是 0 也要一直做,直到每个链表都循环完了,并且进位为 0 为止

技巧:凡是需要特判第一个点的时候,都可以加一个虚拟头节点,这样就不需要特判第一个点(插入的点永远不可能是第一个点),虚拟头节点的值任意,这个点永远不会用到

如果没有定义虚拟头节点,当插入的点是第一个点的时候,需要特判,插入第一个点的时候,这个链表还不存在,需要把它创建出来,就要加 if 特判。定义虚拟头节点后,就不需要特判,因为插入的点永远不可能是第 1 个点

cur 存储和链表的尾节点,每次插入一个新节点需要在尾节点做插入,需要用一个变量存储尾节点是哪一个

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        //定义一个虚拟头节点 做插入的时候就不用判断当前点是不是第一个点 和链表的虚拟头节点
        //cur 表示当前和链表的尾节点
        auto dummy = new ListNode(-1),cur = dummy;
        //进位
        int t = 0;
        //1.l1没有循环完 2.l2没有循环完 3.进位不为0
        while(l1 || l2 || t) {
            //l1没有循环完 把第一个数加上
            if(l1) t += l1->val,l1 = l1->next;
            //l2没有循环完 把第二个数加上
            if(l2) t += l2->val,l2 = l2->next;
            //和的这个位是 t 的个位数字
            //每次插入一个新节点之后 需要把尾节点更新成新节点
            cur = cur->next = new ListNode(t % 10);  
            //进位  
            t /= 10;
        }
        //返回虚拟头节点的next指针 也就是真正的链表头节点
        return dummy->next;
    }
}; 

无重复字符的最长子串

算法标签:哈希表、字符串、滑动窗口

 

 

双指针算法、位运算、离散化、区间合并

给出一个字符串,要找到其中不含有重复字符的最长的一段

枚举样例发现,最长的不包含重复字符的子串长度是 3

双指针、滑动窗口,给出一个序列,考虑怎么把答案找出来(怎么能够把所有情况都枚举到,然后取 max)

一共有 2 / n^2 个不同的子串,在这 2 / n^2 个子串中,找不包含重复字符中的子串里面最长的一个

以这个子串的尾节点分类,可以分成 n 类,子串的最后一个字母在第 0 个位置是第一类,在第 1 个位置是第二类,依次类推,一共可以分成 n 类,每次处理一类即可

 枚举所有以 i 为尾节点的所有子串,在它所有子串中找到最长的一个不包含重复字符的子串

相当于找一个最靠左的 j,使得从 j 到 i 不包含任何一个重复字符

用哈希表找 j 需要 O( n ) 的时间复杂度,需要枚举 j,暴力做法就是 O( n^2 ) 的时间复杂度

看数据有没有单调关系,有单调关系就利用单调关系把 O( n^2 ) 的复杂度变成 O(n)

如果 i 变成 i '(往后移动了一格),  i ' 也会有一个最靠左的 j ',如果 i 往后移动到 i ',i ' 所对应的 j ' 应该也会往后移动 ( 应该在 j 的右边,或者在 j 的位置上 )

反证法:假设 i ' 对应的 j ' 在 j 的左边,也就是说从 i ' 到 j ' 之间是不包含任何一个重复字符,出现矛盾:从 j ' - i ' 之间不包含任何一个重复的字符,从 j ' - i 之间也不包含任何一个重复的字符,所以 j 就可以取到 j ' 的位置,说明 j 不是 i 最靠左的一个满足要求的 j

可以发现:当第二个尾端点往后移动的时候,这个尾端点所对应的一个最靠左的端点一定要么不动,要么往右走

每一次枚举完 i 之后,再枚举一个新的 i ' 的时候,可以从上一个 j 的位置继续往后枚举就可以了,每个指针都会从前往后走不会回头,每个指针最多只会走 n 次,(两个指针最多只会扫描 2n 次)整个算法的时间复杂度为 O(n)

每次 i 往后移动 1 格,就把下一个字母 i + 1 加到哈希表中去,然后看此时集合里面是否有重复元素,由于这里面只把 i + 1 加进去了,如果里面有重复元素,那么重复元素必然是 Si+1,然后就把 j 一直往后移动,直到移动到某一个 Si+1 为止,然后把 Si+1 剔除,保证 Si+1 只有 1个,最终满足要求

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        //定义一个哈希表 统计j - i中每个字符出现的次数
        unordered_map<char,int> heap;
        //定义一个最大值
        int res = 0;
        //从前往后枚举 i 是后面的指针 j 是前面的指针
        for(int i = 0,j = 0;i < s.size();i++) {
            //每次把当前新的字符加到哈希表中去
            heap[s[i]] ++;
            //如果有重复 必然是 s[i] 重复 因为只有 s[i] 变多了 
            //当 s[i] 大于 1 的时候,每一次将 s[j] 删除并把 j 往后移动一位 
            while(heap[s[i]] > 1) heap[s[j++]]--;
            //退出循环之后 当前从 j 到 i 就是以 i 结尾的不包含重复字符最长的一段
            //用这一段更新答案
            res = max(res,i - j + 1);
        }
        return res;
    }
};

寻找两个正序数组的中位数😅

算法标签:数组、二分查找、分治、递归

 

给出两个有序数组,第一个数组的长度是 m,第二个数组的长度是 n,求这两个数组合并之后的中位数

中位数的定义:

如果是奇数个数,中位数是最中间那个数,如果是偶数个数,中位数是最中间两个数的平均数

暴力:把两个数组合并在一块,排序后输出,时间复杂度是 O(nlogn)

法一:递归😎,时间复杂度为 O(log(m + n))

两个有序数组,求当前这两个数组当中从小到大排列,第 k 个数是多少,让 k  = ( n + m ) / 2 就可以求出中位数

原问题难以直接递归求解,所以我们先考虑这样一个问题:

如果该问题可以解决,那么第 ( n + m ) / 2 小的数就是我们要求的中位数

在两个有序数组中,第一个数是最小值,第二个数是次小值,求第 k 小数是多少?

先在这两个数组中找到从小到大排列第 k / 2 个元素,有 3 种情况

第一种情况 A[ k / 2 ] < B[ k / 2 ]:在 A 数组里面 小于 A[ k / 2 ] 的个数 有 k / 2 个,在 B 数组里面小于 A[ k / 2 ] 的个数应该小于 k / 2(由于 A[ k / 2 ] < B[ k / 2 ]),最终小于 A[ k / 2 ] 的个数一定是小于 k,说明 A[ k / 2 ] 一定是在前 k 个数的前面,不可能在第 k 个数的位置,所以这一部分的数一定不会是答案,可以把这一部分删掉(这样就可以删掉 k / 2 个元素了)

 

第二种情况:如果 A[ k / 2 ] > B[ k / 2 ],同理,最终小于 B[ k / 2 ] 的个数也是小于 k,说明 B[ k / 2 ] 一定是在前 k 个数的前面,不可能在第 k 个数的位置,所以这一部分的数一定不会是答案,可以把这一部分删掉(这样就可以删掉 k / 2 个元素了)

第三种情况:如果 A[ k / 2 ] == B[ k / 2 ],这两个值相同,说明 A[ k / 2 ] 或 B[ k / 2 ] 这个值恰好就是第 k 个元素,这两个值都可以作为第 k 个数,删除的时候,可以删除前面的也可以删除后面的

如果 A[ k / 2 ] == B[ k / 2 ],把 A 数组和 B 数组合并在一块去排序,由于这两个数组都是有序的,所以第一个数组里面 k / 2 - 1 个数都是小于等于 A[ k / 2 ],第二个数组里面前 k / 2 - 1 个数都是小于等于 B[ k / 2 ],A 数组 和 B 数组前面一共有 2 × ( k / 2 - 1 ) 个元素,也就是 k - 2 个元素,所以排行在第 k 位的元素一定是 B[ k / 2 ] 或者 A[ k / 2 ]

每次比较完这两个数之后,一定可以删掉 k / 2 个元素,每次删除 k / 2 个元素之后,相当于在红色区间中找到第 k - k / 2 个数,就变成了一个递归的子问题

思路

每次都可以去掉 k / 2 个元素,所以 k 每次会除以 2,当 k = 1 的时候,相当于找两个数组的最小值,比较两个数组的开头元素,取小的那个即可

k 从头减到 1,k 每次除以 2,所以这个做法最多只会递归 log k 次,k 的大小是 ( m + n ) / 2,时间复杂度就是 1 / 2 O(log(m + n)),也就是 O(log(m + n)),时间复杂度不考虑常数

注意

如果存在空数组,直接取任意一个数组的第 k 位元素返回

由于 k 分为奇数和偶数,所以在计算下标时要用 k / 2 和 k - k / 2 ( si 和 sj 是第 k / 2 个元素的下一个位置 )

如果有数组长度不足 k / 2,A[ k / 2 ] == B[ k / 2 ] 的做法就不成立了,需要把相等时的处理合并到 > 或 < 的做法中的任意一组

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        /*
              计算中位数需要根据数组长度进行判断
              1.如果数组长度是偶数,计算中间两个数字的平均值
              2.如果数组长度时奇数,直接返回中间的数字
         */
        //得到两个数组的元素总数
        int tot = nums1.size() + nums2.size();
        //偶数
        if (tot % 2 == 0) {
            //找中间两个数
            //左侧找第 k / 2 个数
            int left = find(nums1, 0, nums2, 0, tot / 2);
            //右侧找第 k / 2 + 1 个数
            int right = find(nums1, 0, nums2, 0, tot / 2 + 1);
            //返回它们的平均值 / 2.0 保证返回的是浮点数
            return (left + right) / 2.0;
        } else {
            //奇数
            //直接返回中间的数字
            return find(nums1, 0, nums2, 0, tot / 2 + 1);
        }
    }
    //找到两数组中第 k 大的元素 第一个数组 开始查找的下标 第二个数组 开始查找的下标 找第 k 大的数
    int find(vector<int>& nums1, int i, vector<int>& nums2, int j, int k) {
        //需要保证 num1 的长度不超过 num2 的长度 
        //假定第一个数组比较短,如果第一个数组比较长就要做交换
        if (nums1.size() - i > nums2.size() - j) return find(nums2, j, nums1, i, k);

        //特判 如果 num1 没有元素,要找的第 k 个数就是 num2 的第 k 个元素,k 从 1 开始
        if (nums1.size() == i) return nums2[j + k - 1];

        //递归终止条件 k = 1 直接返回两数组第一个元素的最小值
        if (k == 1) return min(nums1[i], nums2[j]);

        //num1 对应的下标是si,num2 对应的下标是sj
        //注意 num1 的长度可能不足 k / 2,可能会越界,需要特殊处理
        int si = min((int)nums1.size(), i + k / 2), sj = j + k - k / 2;

        //判断第 k / 2 个元素的后一个元素 si 的前一个元素
        if (nums1[si - 1] > nums2[sj - 1])
            //把 num2 的前 k / 2 个元素删掉,改变第二个数组的起始位置,第一个数组的起始位置没有变,k 要减去被删掉的元素个数
            return find(nums1, i, nums2, sj, k - (sj - j));
        else
            //把 num1 的前 k / 2 个元素删掉
            return find(nums1, si, nums2, j, k - (si - i));
    }
};

法二:二分😭,时间复杂度为 O(log(min(m,n))

最长回文子串

算法标签:字符串、动态规划

 

给出一个字符串,要在这个字符串中找到最长的一个回文子串

马拉车算法,时间复杂度 O(n)

字符串哈希 + 二分,时间复杂度 O(nlogn)

什么是回文串?

按长度分为两种:长度是奇数的回文串:满足左右两边对称,长度是偶数的回文串:两两配对,分别相等

可以先枚举每个回文串的中心,中心确定之后,用两个指针同时从中心开始往两边走,直到走到两个字符不一样,或者某一个指针走出边界为止,这样就可以找到以这个点为中心的最长一个回文串

边走边对比两个指针指向的字符是不是一样就可以了,如果一样就继续走,如果不一样就停止

停下来后,从 [ L + 1,R - 1 ] 这一段就是以 i 为中心的最长的一个回文串,长度应该是 R - 1- ( L + 1 ) + 1

回文串有两种,第一种长度是奇数,第二种长度是偶数

长度是奇数,在初始化的时候,让 L 和 R 从 i 的左右两边一个位置开始走

长度是偶数,L 从 i 开始走,R 从 i + 1 开始走

 

枚举中心需要 O(n) 次操作,枚举左右两个指针也是 O(n),整个算法的时间复杂度就是 O(n^2)

class Solution {
public:
    string longestPalindrome(string s) {
        //表示最长的一个回文子串
        string res;
        //枚举中心点
        for(int i = 0;i < s.size();i++) {
            //枚举长度是奇数的情况
            int l = i - 1,r = i + 1;
            //两个指针分别往两边走 当两个指针都没有越界的时候,并且两个指针指向的数值相同 
            //左指针就往左走一格右指针就往右走一格
            while(l >= 0 && r < s.size() && s[l] == s[r]) l --,r ++;
            //停下来的时候 从[l + 1,r - 1]中间这一段就是最长的一个回文串
            if(res.size() < r - l - 1) res = s.substr(l + 1,r - l - 1);
            //枚举长度是偶数的情况
            l = i,r = i + 1;
            while(l >= 0 && r < s.size() && s[l] == s[r]) l --,r ++;
            //停下来的时候 从[l + 1,r - 1]中间这一段就是最长的一个回文串
            if(res.size() < r - l -1) res = s.substr(l + 1,r - l - 1);
        }
        return res;
    }
};
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qiuqiuyaq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值