Leetcode第一题:用C++解决两数之和问题

前言

大家好!作为一个 C++ 初学者,最近我打算用 C++ 来挑战 Leetcode ,在干中学,从而提升自己的编程和算法能力。今天我完成了第一道题,就是极其经典的两数之和,整个过程下来,我感觉收获很大,在克服重重困难之后也算是对 C++ 的一些特性有了比较浅显的理解。

本篇文章旨在记录我的学习过程,作为笔记我还会记录一些 C++ 的知识点,同时也希望和我一样正在学习 C++ 的初学者朋友们看到后能够有所收获。

1. 理解问题

我们首先来看一下题目,这里我只简略描述一下题目。

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

示例: 输入:nums = [2, 7, 11, 15]target = 9 输出:[0, 1] 解释:因为 nums[0] + nums[1] == 9

2. 暴力解法

作为一个初学者,第一反应往往是最简单朴素的暴力解法,我也不例外。

暴力解法的思路如下:

  1. 我们先拿着数组中第一个元素;
  2. 我们再依次拿起数组中除第一个元素之外的每一个元素;
  3. 第二步拿起的每一个元素都与第一步拿起的元素相加,看看是否等于目标值,如果等于目标值,直接返回两个元素的下标;
  4. 如果都不等于目标值,我们再拿起第二个元素,再和第二个元素之后的每一个元素相加并与目标值比较,以此类推。

我之前一直是使用 C 语言的,但我相信学习过任何一种编程语言的都能看出来,这个思路转换成代码其实就是两层for循环。

具体实现如下:

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

这段代码虽然成功通过了,但是正如前面所说,这种方法是暴力解法,它简单明了,但并不是最高效的解法,如果数组中的元素非常多,那么双层循环的劣势就会极其明显,程序的运行时间会急剧加快。

知识点我会放在最后统一讲,下面先介绍另一种解法。

3. 哈希表解法

想要对暴力解法进行优化,我们首先要意识到暴力解法究竟慢在哪里。

对于 nums[i],我们需要再用一个循环去数组里找 target - nums[i],也就是说,在拿到一个数之后,它在寻找另一个数这个环节中的效率太低。

这就引出了我们第二种解法------哈希表高效解法。它能让我们瞬间知道 target - nums[i]是否在数组里或者在数组的那个位置,这是一种用空间换时间的策略。

于是,我们又有了新的思路:

  1. 创建一个哈希表,用于记录我们已经遍历过的数组元素和它的下标。
  2. 开始用for循环遍历数组,先拿到数组的第一个元素nums[0]
  3. 我们需要找到它的另一半 target - nums[0],我们去哈希表中查一下是否有这个数,现在当然是没有的,因为我们还没有往哈希表中存任何内容,哈希表还是空的。
  4. 由于我们没找到target - nums[0],于是我们把nums[0]和它的数组下标 0 存入哈希表。
  5. 继续num[1],重复上面过程。

这个过程中只需要遍历一次数组,每次查询哈希表的速度都接近 O(1),所以总的时间复杂度降到了 O(n),这是一个巨大的提升。

哈希表解法的具体实现如下:

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int,int> values;
        for(int i=0; i<nums.size(); i++)
        {
            int complement = target - nums[i];
            if(values.find(complement) != values.end())
            {
                return {values[complement],i};
            }
            values[nums[i]] = i;
        }
        return {};
    }
};

到现在为止,两种解法的思路和具体实现都已经介绍完了,其实最重要的是思路。

我们拿到一道题目,首先要在大脑中构建出这道题目的解决思路,然后才能根据需要使用相应的工具来解决这个问题。

下一章我将具体介绍上面代码涉及到的 C++ 知识点,以及我作为一个 C++ 初学者的一些理解。

4. C++知识点梳理

由于我之前主要接触的是 C 语言,所以在刚开始用 C++ 写这道题时,遇到了不少新鲜的概念。借着这道题,我把代码中涉及到的 C++ 核心知识点做了一个详细的梳理。

4.1 类的基本概念

在 Leetcode 中,我们提交的代码通常是被包裹在一个(class)里面的,大家在 Leetcode 上面做题时应该也有观察到这一点。如下:

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        //函数代码
    }
};

在 C 语言中,我们习惯直接写一个个独立的函数。但在 C++ 这种面向对象 的语言中,函数通常作为方法 存在于之中。

C++ 中的类是面向对象编程的核心概念之一,它把数据操作这些数据的方法封装在一起。

类的访问控制主要通过三种访问限定符来实现:分别是publicprivateprotected。这三个关键字决定了类的成员能被谁访问。

class 默认访问权限是 private,而 struct 默认是 public

下面我列表总结一下三种访问权限的特点:

public经常用于函数接口,对外暴露的功能。

private经常用于内部实现细节,以及需要严格隐藏的数据。

protected经常用于给子类用的半公开成员。

4.2 vector及其常用操作

在 C 语言中,数组的长度一旦定义就固定死了,比如 int arr[10]。而 vector 是 C++ 标准模板库 STL 提供的一种动态数组,它可以根据需要自动伸缩,用起来极其方便。

在 C++ 中,vector 实例化出来的变量,比如题目中的 nums 就是一个对象 。对象内部自带了很多现成的函数,我们可以通过在一个对象变量后面加上一个点(.) ,来调用这些函数。

下面列举几个比较常见的操作:

cpp 复制代码
vector<int> nums;//创建一个空的整形动态数组
​
//往数组末尾塞数据
nums.push_back(10);
nums.push_back(20);//现在数组是[10,20]
​
//获取数组长度
int len = nums.size();//现在len的值为2
​
//判断数组是否为空
bool isEmpty = nums.empty();//false,因为数组里面有数据
​
//像普通数组一样通过下标访问
int n = nums[0];

在这道题的暴力解法中,我们正是利用了 nums.size() 来作为 for 循环的边界,再也不用像 C 语言那样需要额外传一个 int length 参数进来了。

4.3 引用传递&

C++ 中的引用传递是现代 C++ 中最常用、最推荐的函数参数传递方式之一。

下面我将通过列表来对比值传递指针传递 以及引用传递三者的特点,以便更好地区分与理解他们:

我们仔细看上面程序中函数的参数:vector<int>& nums。这里面有一个非常关键的符号 &。如果不加这个符号,哪怕逻辑全对,在 Leetcode 上也可能因为运行超时而失败。由上表中我们可以看到,值传递是需要拷贝的,而引用传递不需要拷贝。如果我们不加 & 符号,Leetcode 的测试程序在调用twoSum函数时,就会将原本的nums数组再拷贝一份进行操作,试想一下,假如nums数组中有一万个元素,会是怎样的后果?

除此之外,还需要再严格区分一下引用指针。引用本质上只是一个别名,不占据独立的内存,但是修改这个别名就是修改原始的数据。而指针是占据独立内存的。

4.4 列表初始化返回

当我们在暴力解法中找到答案时,我直接写了 return {i, j};

但是最初,我是按照 C 语言的思维,用 C++ 的方式来写的,如下:

cpp 复制代码
vector<int> result;//先创建一个空的vector对象
result.push_back(i);//把i塞进去
result.push_back(j);//把j塞进去
return result;

但在现代 C++ 中,引入了列表初始化 。因为编译器预先知道这个函数的返回值类型是 vector<int>,所以当写下 {i, j} 时,编译器会在底层自动完成上面那 4 步繁琐的操作。

同理,当找不到答案时,直接 return {}; 就会返回一个空的 vector,非常简洁。

4.5 C++中的哈希表

在 C++ 里,哈希表叫 unordered_map,字面意思是无序映射

字典的目录应该可以算做一种哈希表。如果字典没有目录,我们在查询单词abandon时,就需要一页一页去找。但是我们完全可以多花费几页纸,来做一个目录,列出某些单词与页码的对应关系,从而加快查找速度。这就是用空间换时间

哈希表存的是键值对

cpp 复制代码
//unordered_map<键, 值> 哈希表名称
unordered_map<int, int> values;

前一个 int键 Key :用来存数组里实际的数字大小

后一个 int值 Value :用来存这个数字对应的数组下标

在哈希表中找东西的操作如下:

cpp 复制代码
if(values.find(complement) != values.end())

依然是对象加点的操作。

values.find(目标数字):这个函数去哈希表里找目标数字。如果找到了,它会返回一个迭代器Iterator,它指向了哈希表中存有那个数字的具体位置。

values.end():它代表哈希表的末尾的下一个空气位置

综合起来看,这行代码就是 C++ 中判断哈希表里是否存在某个数的最标准写法。

然后我们再来看哈希表的插入与访问

在没找到配对的数字时,我们需要把当前的数字记录到哈希表里:

cpp 复制代码
values[nums[i]] = i;

熟悉 C 语言数组的人可能会和我最初一样觉得疑惑,哈希表又不是普通数组,怎么也能用中括号 []这样操作?

这就是 C++ 中括号 运算符对哈希表的强大重载。这个简单的等式兼具了两种功能

  1. 如果 nums[i]在哈希表里不存在 ,它会自动创建一个新的键值对 ,把键设为 nums[i],把值设为 i
  2. 如果数字 nums[i] 已经存在 于哈希表中了,它不会报错,而是会直接把旧的下标覆盖掉,更新为现在的下标 i

在这道题里,我们就是利用了它的自动创建能力,一边遍历数组,一边把不满足条件的数字记录在哈希表里,供后面的数字随时查询。

5. 总结

虽然只是 Leetcode 的第一题,但我花了比预期更多的时间去查资料、理解面向对象思维、熟悉哈希表的底层逻辑以及 C++ 的各种语法逻辑。但我想这也许就是"万事开头难"这句话的现实体现吧。

希望这篇详细到有些啰嗦的记录,也能帮助到正在看文章并且同为 C++ 初学者的你。C++ 学习之路道阻且长,有缘的话我们下一题见!

相关推荐
不想写代码的星星1 天前
C++继承、组合、聚合:选错了是屎山,选对了是神器
c++
不想写代码的星星2 天前
std::function 详解:用法、原理与现代 C++ 最佳实践
c++
樱木Plus4 天前
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)
c++
blasit6 天前
笔记:Qt C++建立子线程做一个socket TCP常连接通信
c++·qt·tcp/ip
肆忆_7 天前
# 用 5 个问题学懂 C++ 虚函数(入门级)
c++
不想写代码的星星7 天前
虚函数表:C++ 多态背后的那个男人
c++
端平入洛9 天前
delete又未完全delete
c++
端平入洛10 天前
auto有时不auto
c++
琢磨先生David11 天前
Day1:基础入门·两数之和(LeetCode 1)
数据结构·算法·leetcode