1 两数之和

内容来自 :力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现

你可以按任意顺序返回答案。(数组下标从0开始)

1-3_暴力枚举1-3_链表_02

暴力枚举1:

具体步骤如下:

  1. 使用两层循环遍历数组 nums 的所有可能组合。(放大叫解空间吗?)
  2. 对于每一对组合 (nums[i], nums[j]),判断它们的和是否等于目标值 target。(找可行)
  3. 如果找到了满足条件的一对数字,则返回它们的下标。

以下是使用伪代码表示的暴力枚举方法:

function twoSum(nums, target):
    for i from 0 to length(nums)-2:
        for j from i+1 to length(nums)-1:
            if nums[i] + nums[j] == target:
                return [i, j]
    return null

暴力枚举的时间复杂度为 O(n^2),空间复杂度为 O(1)。

暴力枚举2:

除了上述的双重循环暴力枚举方法之外,还可以使用单层循环的暴力枚举方法。具体步骤如下:

  1. 使用单层循环遍历数组 nums 的每个元素。
  2. 对于当前元素 nums[i],从 i+1 开始遍历数组的剩余部分。
  3. 对于每个元素 nums[j],判断 nums[i] + nums[j] 是否等于目标值 target。
  4. 如果找到了满足条件的一对数字,则返回它们的下标。

以下是使用伪代码表示的单层循环暴力枚举方法:

function twoSum(nums, target):
    for i from 0 to length(nums)-2:
        for j from i+1 to length(nums)-1:
            if nums[i] + nums[j] == target:
                return [i, j]
    return null

这种暴力枚举方法的时间复杂度也是 O(n^2),但相比于双重循环,减少了一些重复计算。

暴力枚举3:

除了上述两种暴力枚举方法之外,还可以使用逆向枚举的方法。具体步骤如下:

  1. 首先固定一个数,从数组的最后一个元素开始遍历。
  2. 对于当前固定的数 nums[i],从 i-1 开始向数组的开头遍历。
  3. 对于每个元素 nums[j],判断 nums[i] + nums[j] 是否等于目标值 target。
  4. 如果找到了满足条件的一对数字,则返回它们的下标。

以下是使用伪代码表示的逆向枚举方法:

function twoSum(nums, target):
    for i from length(nums)-1 to 1:
        for j from i-1 to 0:
            if nums[i] + nums[j] == target:
                return [j, i]
    return null

逆向枚举的时间复杂度也是 O(n^2),但与正向枚举不同的是,它从后往前遍历,可能会在某些特定情况下提供更好的效率。然而,在输入规模较大时,仍然存在较高的时间复杂度。

最容易想到的方法是枚举数组中的每一个数 x,寻找数组中是否存在 target - x。

当我们使用遍历整个数组的方式寻找 target - x 时,需要注意到每一个位于 x 之前的元素都已经和 x 匹配过,因此不需要再进行匹配。而每一个元素不能被使用两次,所以我们只需要在 x 后面的元素中寻找 target - x。

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

1-3_暴力枚举_03

方法二:哈希表 思路及算法

注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。

使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N)降低到 O(1)。

这样我们创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。

哈希表(Hash Table),也被称为散列表,是一种数据结构,用于实现键值对(key-value pairs)的关联数组。它通过将关键字(key)映射到一个固定大小的数组中来实现高效的查找和插入操作。

哈希表的核心思想是使用哈希函数将关键字映射到数组的特定位置,该位置称为哈希桶(hash bucket)或槽(slot)。当需要查找一个键对应的值时,只需计算出该键的哈希值,并根据哈希值找到对应的桶,然后在桶内进行查找即可,这样可以大大提高查找的效率。在理想情况下,哈希函数能够将不同的键均匀地映射到不同的桶中,避免冲突,但实际情况下可能会存在冲突,即多个键映射到了同一个桶内。

解决哈希冲突的常见方法有两种:开放寻址法和链表法。开放寻址法是指当发生冲突时,继续探测哈希表中的下一个空槽,直到找到空槽或者遍历整个哈希表;链表法是指每个桶内维护一个链表或其他数据结构,将冲突的元素都存储在其中。

哈希表的时间复杂度通常是O(1),即在大部分情况下,查找、插入和删除操作的平均时间复杂度都是常数级别的。因此,哈希表被广泛应用于各种编程任务,如缓存实现、数据库索引等。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> hashtable;
        for (int i = 0; i < nums.size(); ++i) {
            auto it = hashtable.find(target - nums[i]);
            if (it != hashtable.end()) {
                return {it->second, i};
            }
            hashtable[nums[i]] = i;
        }
        return {};
    }
};

std::vector 是C++标准库中的一个容器类用于存储和操作动态数组。它提供了许多功能和方法,用于方便地管理数组的大小、插入、删除和访问元素等操作。

以下是一些常用的 std::vector 操作:

  • 创建 std::vector 对象:可以使用构造函数或赋值运算符来创建 std::vector 对象。
  • 获取向量的大小:可以使用 size() 方法获取 std::vector 中的元素数量,例如:
  • 访问向量的元素:可以使用下标操作符 [] 或 at() 方法来访问 std::vector 中的元素
  • 向向量中添加元素:可以使用 push_back() 方法将元素添加到 std::vector 的末尾
  • 删除向量中的元素:可以使用 pop_back() 方法删除 std::vector 的末尾元素
  • 遍历向量中的元素:可以使用循环结构(如 for 循环或范围循环)来遍历 std::vector 中的元素
std::vector<int> nums; // 创建一个空的整数向量
std::vector<int> nums2 = {1, 2, 3, 4}; // 创建一个包含初始元素的整数向量
std::vector<int> nums3(nums2); // 通过复制构造函数创建一个新的整数向量
///
int size = nums.size(); // 获取 nums 的元素数量
///
int firstElement = nums[0]; // 获取第一个元素
int secondElement = nums.at(1); // 获取第二个元素
///
nums.push_back(5); // 在 nums 的末尾添加一个元素 5
///
nums.pop_back(); // 删除 nums 的末尾元素
//
for (int i = 0; i < nums.size(); ++i) {
    int element = nums[i]; // 访问索引为 i 的元素
    // 在这里进行操作...
}

for (int element : nums) {
    // 使用 element 进行操作...
}

1-3_链表_04

2 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。1-3_链表_05

mod  取模    

|| 运算符表示逻辑或操作。如果至少一个操作数为真,它就返回真。因此,只有当 l1l2 同时为假时,循环才会停止。

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode *head = nullptr, *tail = nullptr;   //定义了两个指针head和tail,分别用于指向结果链表的头节点和尾节点
        int carry = 0;     //carry表示进位的值,初始化为0。
        while (l1 || l2) { //进入循环,当l1或l2不为空时继续执行。在每次循环中,获取l1和l2当前节点的值,并将其与进位值相加得到sum。
            int n1 = l1 ? l1->val: 0;//如果 l1 存在(即非空指针),则将 l1 的值赋给 n1,否则将 0 赋给 n1
            int n2 = l2 ? l2->val: 0;
            int sum = n1 + n2 + carry;
            //如果结果链表还没有头节点,说明当前节点是头节点,将head和tail都指向当前节点。
            //否则,将当前节点添加到结果链表的尾部,更新tail指针。
            if (!head) {
                head = tail = new ListNode(sum % 10);
            } else {
                tail->next = new ListNode(sum % 10);
                tail = tail->next;
            }
            //更新进位值carry为sum除以10的商。同时,将l1和l2指向下一个节点。
            carry = sum / 10;
            if (l1) {
                l1 = l1->next;
            }
            if (l2) {
                l2 = l2->next;
            }
        }
        //循环结束后,如果进位值大于0,说明还有一位需要进位,将其添加到结果链表的尾部。
        if (carry > 0) {
            tail->next = new ListNode(carry);
        }
        //返回结果链表的头节点。
        return head;
    }
};

1-3_链表_06

3 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。(最长子串,连续,最长子序列,不连续)1-3_链表_071-3_暴力枚举_08

1-3_数组_09

如果我们依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的!这里的原因在于,假设我们选择字符串中的第 kkk 个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为 rkr_kr k 。那么当我们选择第 k+1k+1k+1 个字符作为起始位置时,首先从 k+1k+1k+1 到 rkr_kr k 的字符显然是不重复的,并且由于少了原本的第 kkk 个字符,我们可以尝试继续增大 rkr_kr k ,直到右侧出现了重复字符为止。

这样一来,我们就可以使用「滑动窗口」来解决这个问题了:

我们使用两个指针表示字符串中的某个子串(或窗口)的左右边界,其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 rkr_kr k ;

在每一步的操作中,我们会将左指针向右移动一格,表示 我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;

在枚举结束后,我们找到的最长的子串的长度即为答案。

判断重复字符

在上面的流程中,我们还需要使用一种数据结构来判断 是否有重复的字符,常用的数据结构为哈希集合(即 C++ 中的 std::unordered_set,Java 中的 HashSet,Python 中的 set, JavaScript 中的 Set)。在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。

1-3_链表_10

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // 哈希集合,记录每个字符是否出现过
        unordered_set<char> occ;
        int n = s.size();
        // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
        int rk = -1, ans = 0;
        // 枚举左指针的位置,初始值隐性地表示为 -1
        for (int i = 0; i < n; ++i) {
            if (i != 0) {
                // 左指针向右移动一格,移除一个字符
                occ.erase(s[i - 1]);
            }
            while (rk + 1 < n && !occ.count(s[rk + 1])) {
                // 不断地移动右指针
                occ.insert(s[rk + 1]);
                ++rk;
            }
            // 第 i 到 rk 个字符是一个极长的无重复字符子串
            ans = max(ans, rk - i + 1);
        }
        return ans;
    }
};

哈希集合(Hash Set)是一种数据结构,它存储一组唯一的元素,并且提供高效的插入、删除和查找操作。在C++中,可以使用 std::unordered_set 来实现哈希集合。

  • 创建 std::unordered_set 对象:可以使用构造函数或赋值运算符来创建 std::unordered_set 对象。
  • 插入元素:可以使用 insert() 方法将元素插入到 std::unordered_set 中
  • 删除元素:可以使用 erase() 方法删除 std::unordered_set 中的元素
  • 查询元素:可以使用 find() 方法查找元素是否存在于 std::unordered_set 中
  • 获取集合的大小:可以使用 size() 方法获取 std::unordered_set 中的元素数量
  • 遍历集合中的元素:可以使用范围循环(range-based for loop)或迭代器来遍历 std::unordered_set 中的元素
std::unordered_set<int> mySet; // 创建一个空的整数哈希集合
std::unordered_set<int> mySet2 = {1, 2, 3, 4}; // 创建一个包含初始元素的整数哈希集合

mySet.insert(5); // 插入元素 5 到 mySet 中

mySet.erase(3); // 删除元素 3

auto it = mySet.find(2); // 查找元素 2
if (it != mySet.end()) {
    // 元素找到
} else {
    // 元素不存在
}

int size = mySet.size(); // 获取 mySet 的元素数量

//遍历集合中的元素
for (const auto& element : mySet) {
    // 使用 element 进行操作...
}

for (auto it = mySet.begin(); it != mySet.end(); ++it) {
    const auto& element = *it; // 获取迭代器指向的元素
    // 使用 element 进行操作...
}