【Algorithm】Day-11

本篇文章主要讲解算法练习题


1 数组中的 k-diff 数对

链接532. 数组中的 k-diff 数对 - 力扣(LeetCode)

题目描述

给你一个整数数组 nums 和一个整数 k,请你在数组中找出不同的 k-diff 数对,并返回不同的 k-diff 数对 的数目。

k-diff 数对定义为一个整数对 (nums[i], nums[j]),并满足下述全部条件:

  • 0 <= i, j < nums.length
  • i != j
  • |nums[i] - nums[j]| == k

注意|val| 表示 val 的绝对值。

示例 1:

复制代码
输入:nums = [3, 1, 4, 1, 5], k = 2
输出:2
解释:数组中有两个 2-diff 数对, (1, 3) 和 (3, 5)。
尽管数组中有两个 1 ,但我们只应返回不同的数对的数量。

示例 2:

复制代码
输入:nums = [1, 2, 3, 4, 5], k = 1
输出:4
解释:数组中有四个 1-diff 数对, (1, 2), (2, 3), (3, 4) 和 (4, 5) 。

示例 3:

复制代码
输入:nums = [1, 3, 1, 5, 4], k = 0
输出:1
解释:数组中只有一个 0-diff 数对,(1, 1) 。

提示:

  • 1 <= nums.length <= 104
  • -107 <= nums[i] <= 107
  • 0 <= k <= 107

题目解析

这道题目会给你一个整数数组 nums 以及一个整数 k,题目要求你求出 nums 中不同的 k-diff 数对。其中 k-diff 数对是指:abs(nums[i] - nums[j]) == k,并且 i != j。比如 nums = [1, 3, 1, 5, 7, 9],k = 2,返回值就是4,数对分别为 (1, 3)、(3, 5)、(5, 7)、(7, 9)。值得注意的是,虽然 nums 中有两个1,但是返回的是不同的数对。

算法讲解

我们可以先来想一个暴力解法,我们利用一个 i 来遍历 nums,然后 j 从 i + 1 开始遍历nums,当 abs(nums[i] - nums[j]) == k 时,我们就可以 ++ret,但是在遍历过程中,因为要求出不同的数对,所以我们需要对遍历到的元素进行去重,我们可以采用 unordered_set (底层结构是哈希表,可以通过 count 函数快速判断元素的个数)哈希表来进行去重,在去重过程中采用 st1 对 nums[i] 进行去重,采用 st2 对 nums[j] 进行去重。但是这样的话还是可能出现重复数对,比如 nums = [1,2,4,4,3,3,0,9,2,3],当 i = 4 时,此时会添加进一个数对 (3, 0),但是当 i = 6 时,依然会添加进 (0, 3) 这个数对,所有就重复了,为了避免这种情况,所以我们先对数组进行排序,将所有重复的元素放到一起,这样就会避免这种情况:

cpp 复制代码
class Solution 
{
public:
    int findPairs(vector<int>& nums, int k) 
    {
        sort(nums.begin(), nums.end());
        
        int ret = 0;
        unordered_set<int> st1;
        for (int i = 0; i < nums.size() - 1; i++)
        {
            //先判断之前是否已经用过该数字了
            if (st1.count(nums[i])) continue;
            st1.emplace(nums[i]);

            //每次都需要重新创建 st2
            unordered_set<int> st2;
            for (int j = i + 1; j < nums.size(); ++j)
            {
                if (st2.count(nums[j])) continue;
                st2.emplace(nums[j]);
                if (abs(nums[i] - nums[j]) == k) ++ret;
            }
        }

        return ret;
    }
};

显然,时间复杂度是 O(n^2) 的,会超时,所以我们必须对该算法进行优化。

在暴力解法中,我们对数组进行了排序,那么我们就可以利用这一特性来进行优化。我们假设 i 走到了 nums[i],j 走到了 nums[j],由于数组有序,所以如果 nums[j] - nums[i] > k,那么 j 及 j 之后的元素都不可能与 nums[i] 组成 k-diff 数对,所以此时我们直接让 i 向前走就可以了;但是如果 nums[i] - nums[j] < k,那么 i 及 i 之后的元素就不可能与 nums[j] 组成 k-diff 数对,所以我们直接让 j 向后走就可以了。分析到这,我们就可以发现其优化算法为排序 + 双指针

我们先定义两个指针 prev 与 cur,我们初始让 prev = 0, cur = 1,因为数对不能是一个数字,然后我们进行判断,如果 nums[cur] - nums[prev] > k,那我们就直接 ++prev,但是需要注意一点,就是 prev 相加之后不能与 cur 相等,所以我们还需要加一个判断条件,那就是 prev < cur - 1;如果 nums[cur] - nums[prev] < k,那我们就 ++cur,如果 nums[cur] - nums[prev] == k,那么我们就 ++count,但是这个时候是需要去重的,也就是判断 nums[cur] == nums[cur + 1],++cur,但是需要判断是否越界,也就是 cur + 1 < nums.size(),cur 去重完成之后,会处于使得 nums[cur] - nums[prev] == k 的最后一个 cur 相等的位置;同时我们也需要对 prev 进行去重,也就是判断 nums[prev] == nums[prev + 1],++prev,但是 prev 需要小于 cur,也就是 prev < cur;去重完成之后,需要 ++cur,因为 cur 还处于最后一个与之前元素相同的位置。除了上面三种情况,直接 ++cur 就好了。上述过程如图所示:

cpp 复制代码
class Solution 
{
public:
    int findPairs(vector<int>& nums, int k) 
    {
        if (nums.size() < 2) return 0;
        //先对数组排序
        sort(nums.begin(), nums.end());

        int prev = 0, cur = 1;
        int count = 0;
        while (cur < nums.size())
        {
            if  (prev < cur - 1 && nums[cur] - nums[prev] > k) ++prev;
            else if (nums[cur] - nums[prev] == k)
            {
                ++count;
                while (cur + 1 < nums.size() && nums[cur] == nums[cur + 1]) ++cur;
                while (prev < cur && nums[prev] == nums[prev + 1]) ++prev;
                ++cur;
            }
            else ++cur;
        }

        return count;
    }
};

显然,上述代码的时间复杂度为 O(nlogn)。

那么我们可不可以只利用 unordered_set 这种数据结构呢?当然是可以的,因为 unordered_set 这种数据结构可以去重,而且可以快速判断出某个数字是否存在在集合当中,那么我们就可以创建两个 unordered_set,st1 与 st2,st1 用来存储 nums 中的元素,st2 用来存储与 nums[i] 的差的绝对值等于 k 的元素,当遍历到 nums[i] 时,我们就判断 st1 中是否存在 nums[i] - k 与 nums[i] + k,如果存在,说明 nums[i] - k 可以与 nums[i] 构成一个 k-diff 数对,此时将 nums[i] - k 插入到 st2 中,这样就保存了一个数对的较小值;nums[i] + k 与 nums[i] 也可以构成一个 k-diff 数对,我们就可以将 nums[i] 插入到 st2 中,这样也保存了一个数对的较小值,最后返回 st2.size() 就可以了。

代码

cpp 复制代码
class Solution 
{
public:
    int findPairs(vector<int>& nums, int k) 
    {
        unordered_set<int> st1, st2;
        for (auto& x : nums)
        {
            if (st1.count(x - k)) st2.emplace(x - k);
            if (st1.count(x + k)) st2.emplace(x);

            st1.emplace(x);
        }

        return st2.size();
    }
};

时间复杂度为 O(n)。


2 验证回文串 ||

链接680. 验证回文串 II - 力扣(LeetCode)

题目描述

给你一个字符串 s最多 可以从中删除一个字符。

请你判断 s 是否能成为回文字符串:如果能,返回 true ;否则,返回 false

示例 1:

复制代码
输入:s = "aba"
输出:true

示例 2:

复制代码
输入:s = "abca"
输出:true
解释:你可以删除字符 'c' 。

示例 3:

复制代码
输入:s = "abc"
输出:false

提示:

  • 1 <= s.length <= 105
  • s 由小写英文字母组成

题目解析

该题目是在是否是回文串的基础上来进阶了一下。我们依然是判断一个字符串 s 是否是回文串,如果其不是回文串,那就删除一个字符,看剩下的 s 是否是回文串。比如 s = "abcdba",返回 true,因为删除字符 c 或者字符 d 之后,剩下的字符就是一个回文串。

算法讲解

之前我们判断一个字符串是否是回文串,采用的是双指针算法,定义一个 left 与 right 指针,如果 s[left] == s[right],那么我们就 ++left,--right,如果 s[left] != s[right],那就 return false,直到 left >= right 了都没有返回 false,那就返回 true。

那么这个题目可不可以使用双指针算法呢?那么我们试一下,我们依然采用 left 和 right 两个指针,要判断 s 是否是回文串,前面是一样的,当 s[left] == s[right] 时,我们依然 ++left,--right,当 s[left] != s[right] 时,要删除一个字符,其实就是要么删除 s[left],要么删除 s[right] 嘛,所以我们就接着判断 [left + 1, right] 区间或者 [left, right -1] 区间是不是回文串就可以了。所以这个题目依然可以采用双指针算法,只不过要额外编写一个判断 s 字符串 [left, right] 区间是不是回文串的函数。

代码

cpp 复制代码
class Solution 
{
public:
    bool isReverse(string& s, int left, int right)
    {
        while (left < right)
        {
            if (s[left] == s[right]) ++left, --right;
            else return false;
        }

        return true;
    }

    bool validPalindrome(string s) 
    {
        int left = 0, right = s.size() - 1;
        int count = 1;
        while (left < right)
        {
            if (s[left] == s[right]) ++left, --right;
            else return isReverse(s, left + 1, right) || isReverse(s, left, right - 1);
        }

        return true;
    }
};

3 寻找旋转排序数组中的最小值

链接153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)

题目描述

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

复制代码
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:

复制代码
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

示例 3:

复制代码
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

提示:

  • n == nums.length
  • 1 <= n <= 5000
  • -5000 <= nums[i] <= 5000
  • nums 中的所有整数 互不相同
  • nums 原来是一个升序排序的数组,并进行了 1n 次旋转

题目解析

题目会给你一个数组 nums,这个数组是由一个单调递增且没有重复元素的数组旋转了 n 次得来的,比如:arr = [0, 1, 2, 3, 4, 5, 6, 7],那么 nums 可能是由 arr 旋转一次得来,那么 nums = [7, 0, 1, 2, 3, 4, 5, 6],如果旋转两次,那么 nums = [6, 7, 0, 1, 2, 3, 4, 5]。题目要求让我们找到 nums 中的最小元素并且返回。比如 nums = [6, 7, 0, 1, 2, 3, 4, 5],那么返回值就是 0。

算法讲解

暴力解法很简单,就是我们使用一个 minnum 变量,遍历一遍数组,minnum = min(minnum, nums[i]),这样遍历一遍 nums 之后,minnum 中保存的就是最小值。显然,时间复杂度是 O(n) 的。

在暴力解法中并没有应用数组的特性,分析数组,我们可以发现数组中前面是单调递增的,到达一段之后,后面也是单调递增的,显然数组具有二段性,所以我们就可以采用二分查找算法了。那么具体的二段性是什么呢?由于 nums 中没有重复的数字,而且是由一个单调递增的数组旋转而来,假设两段分别为 [0, i] 与 [i + 1, n - 1],那么 [0, i] 中的 nums[j] >= nums[0],而 [i + 1, n - 1] 中的 nums[j] < nums[0],所以我们就可以依次来划分 nums。当 mid 落在左边区间时,也就是 nums[mid] >= nums[0] 时,此时 mid 及 mid 之前的元素绝对不可能是最小值,因为 nums[0] 会大于最小值,所以要 left = mid + 1,而如果 nums[mid] < nums[0],那是可能出现最小值的,所以需要 right = mid。分析出了两个区间的指针移动情况,那么判断条件就是 left < right,而且 left = mid + 1,出现了 + 1,上面求中点就不应该 + 1,也就是 mid = left + (right - left) / 2。

但是还有一种特殊情况我们需要进行判断,那就是 nums == arr 的情况,这种情况下 nums 是一个单调递增的数组,nums[mid] 始终大于 nums[0],所以 left 会始终等于 mid + 1,最后会与 right 重合,所以此时返回的是最大值。所以在返回结果时我们需要特殊判断一下,如果 nums[left] > nums[0],那就返回 nums[0],否则就返回 nums[left]。

代码

cpp 复制代码
class Solution 
{
public:
    int findMin(vector<int>& nums) 
    {
        int n = nums.size();
        int left = 0, right = n - 1;
        while (left < right)
        {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= nums[0]) left = mid + 1;
            else right = mid;
        }
    
        return nums[left] > nums[0] ? nums[0] : nums[left];
    }
};
相关推荐
薛慕昭3 小时前
C语言核心技术深度解析:从内存管理到算法实现
c语言·开发语言·算法
.ZGR.3 小时前
第十六届蓝桥杯省赛 C 组——Java题解1(链表知识点)
java·算法·链表·蓝桥杯
近津薪荼3 小时前
每日一练 1(双指针)(单调性)
c++·算法
林太白3 小时前
八大数据结构
前端·后端·算法
爱思德学术3 小时前
第二届中欧科学家论坛暨第七届人工智能与先进制造国际会议(AIAM 2025)在德国海德堡成功举办
人工智能·算法·机器学习·语言模型
机器学习之心3 小时前
MATLAB多子种群混沌自适应哈里斯鹰算法优化BP神经网络回归预测
神经网络·算法·matlab
qq_479875433 小时前
C++ ODR
java·开发语言·c++
MicroTech20254 小时前
微算法科技(NASDAQ MLGO)“自适应委托权益证明DPoS”模型:重塑区块链治理新格局
科技·算法·区块链
FanXing_zl5 小时前
在整数MCU上实现快速除法计算:原理、方法与优化
单片机·嵌入式硬件·mcu·算法·定点运算