由力扣90.子集II引发的对于C#中的List和C++中的vector区别的思考

引言:解题的思考过程

 昨天的力扣每日一题: 90.子集II
 最初我看到这一题时,想到了在 1178.猜字谜 中学到的位掩码,使用位掩码可以将集合压缩为一个二进制数,从而能够很方便地求出这个集合的子集。
 在对这一题进行编码求解时,我最初使用语言的是C#,力扣提供的C#模板是这样的:

public class Solution {
    public IList<IList<int>> SubsetsWithDup(int[] nums) {
    }
}

可以看到返回值是IList<IList<int>>类型,也就是说要使用一个List来存储求得的结果,于是按照我的思路,最初的代码写成了下面这样:

public static IList<IList<int>> SubsetsWithDup(int[] nums)
{
    IList<IList<int>> ansList = new List<IList<int>>();
    int bitMask = (int)Math.Pow(2, nums.Length) - 1;
    int subset = bitMask;
    do
    {
        IList<int> tempList = new List<int>();
        subset = (subset - 1) & bitMask;//其中每一次循环得到的subset就是mask的全部子集,不会重复
        for(int i = 0; i < nums.Length; i++)
        {
            if (((subset >> i) & 1) == 1)
            {
                tempList.Add(nums[i]);
            }
        }//得到一个子集
        if (!ansList.Contains(tempList))//问题的关键在这一行
        {
            ansList.Add(tempList);
        }//如果子集不重复,则将其加入ansList
    } while (subset != bitMask);
    return ansList;
}

&emsp;但是在对示例[1,2,2]进行测试时,得到的返回结果是这样的:
4-1-1.png
&emsp;可以看到,重复的列表[2][1,2]并没有被检测出来,即使他们与已有的列表中的数据和顺序都完全一致。当时我隐约觉得这可能和List是引用类型有关,从而导致在将两个List进行比较时无法得到我想要的结果。
&emsp;于是,为了赶在12点之前写完代码提交,又不想直接抄一份交上去,我选择了换成C++碰碰运气。
&emsp;根据以往的经验,力扣的题目在C#的方法返回值是IList时,C++的方法返回值应该是vector。果然,C++的模板如下:

class Solution {
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
    }
};

&emsp;把C#代码对应地改为C++代码如下:

class Solution {
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<vector<int>> ansList;
        int bitMask = (int)pow(2, nums.size()) - 1;
        int subset = bitMask;
        do
        {
            vector<int> tempList;
            subset = (subset - 1) & bitMask;//其中每一次循环得到的subset就是mask的全部子集,不会重复
            for (int i = 0; i < nums.size(); i++)
            {
                if (((subset >> i) & 1) == 1)
                {
                    tempList.push_back(nums[i]);
                }
            }
            //此处检测Vector中是否存在某元素这一操作使用了(百度随便找的)迭代器
            vector<vector<int>>::iterator ret = find(ansList.begin(), ansList.end(), tempList);
            if (ret == ansList.end())
            {
                ansList.push_back(tempList);
            }
        } while (subset != bitMask);
        return ansList;
    }
};

&emsp;对[1,2,2]这一示例进行测试发现得到的返回结果是正确的,猜对了(
&emsp;但是倒在了这个输入前面:
4-1-2.png
&emsp;检查发现应该是对于两个vector,如果其中的数据相同但是顺序不同,会被视作两个不同的vector,于是加入下面的代码来在每次将tempList存入ansList前,对tempList进行排序:

sort(tempList.begin(), tempList.end());

&emsp;然后终于通过了,虽然运行时间只击败了6%(
&emsp;那么今天就来研究一下为什么C#中的两个List即使包含的数据和顺序在都一致时会被判断为不相等,以及C++中两个vector为什么能被判断为相等。

对C#中的List的思考研究

&emsp;根据最初的代码内容,想要找到这个答案就应该从List<T>.Contains()方法入手。
&emsp;在查阅了微软的C#文档后,发现其中提到:

&emsp;The List class uses both an equality comparer and an ordering comparer.
&emsp;Methods such as Contains, IndexOf, LastIndexOf, and Remove use an equality comparer for the list elements. The default equality comparer for type T is determined as follows. If type T implements the IEquatable generic interface, then the equality comparer is the Equals(T) method of that interface; otherwise, the default equality comparer is Object.Equals(Object).

&emsp;官方机翻:

&emsp;List类使用相等比较器和排序比较器。
&emsp;诸如Contains、IndexOf、LastIndexOf 和Remove等方法,对列表元素使用相等比较器。 按如下方式确定类型的默认相等比较器 T 。如果类型 T 实现 IEquatable 泛型接口,则相等比较器是 Equals(T) 该接口的方法; 否则,默认的相等比较器为 Object.Equals(Object)。

&emsp;也就是说List如果继承了IEquatable接口,只要看一下List<T>.Equals(T)是如何实现的就行了。查看List的原码,发现List并没有继承IEquatable接口,也就是说两个List在进行比较时,使用的是Object类的Object.Equals(Object)方法。
&emsp;对于这个方法,官方文档的描述是这样的:

如果指定的对象等于当前对象,则为 true;否则为 false。

&emsp;这句说了和没说一样的话并不能解决眼下的问题,不过根据以往的积累,如果进行比较的两个对象是值类型,那么比较的是两个对象的值;如果进行比较的两个对象是引用类型,那么比较的是两个对象的引用(也就是内存空间)。
&emsp;List类型当然不是值类型,那么两个List无论如何进行比较,得到的结果都必然是不相等。
&emsp;(为了严谨还是测试一下)

4-1-3.png
&emsp;结果是False

4-1-4.png
&emsp;结果是True
&emsp;到这里,两个List无法比较的问题已经解决了。

对C++中的vector的思考研究

&emsp;根据C++版本的代码中的内容,我们应该看看find()方法的实现。

&emsp;find() 函数本质上是一个模板函数,用于在指定范围内查找和目标元素值相等的第一个元素。
&emsp;如下为 find() 函数的语法格式:
InputIterator find (InputIterator first, InputIterator last, const T& val);
&emsp;其中,first 和 last 为输入迭代器,[first, last) 用于指定该函数的查找范围;val 为要查找的目标元素。
&emsp;正因为 first 和 last 的类型为输入迭代器,因此该函数适用于所有的序列式容器。
&emsp;另外,该函数会返回一个输入迭代器,当 find() 函数查找成功时,其指向的是在 [first, last) 区域内查找到的第一个目标元素;如果查找失败,则该迭代器的指向和 last 相同。
&emsp;值得一提的是,find() 函数的底层实现,其实就是用= =运算符将 val 和 [first, last) 区域内的元素逐个进行比对。这也就意味着,[first, last) 区域内的元素必须支持= =运算符。

(不是,两个= =中间怎么会标黄的啊(==这样==

&emsp;那么来看看vector有没有重载= =运算符
&emsp;根据 这篇博客 ,虽然微软的C++文档中没有写vector重载了= =运算符,但实际上vector是重载了= =运算符的。

*虽然卡文上没有写,但是这张怪兽可以特殊召唤((无端联想

&emsp;两个vector的比较,正如上文中说到的那样,在元素相同且元素排列顺序相同时,返回的结果为1,否则为0。
&emsp;测试:
4-1-5.png
&emsp;输出为1
&emsp;输出为0的比较懂得都懂,就不放图了。

总结

&emsp;C#中的List<T>没有继承IEquatable<T>接口,所以两个List在使用Equals()方法进行比较时,使用的是Object类的Equals()方法,两个没有重载Equals()方法的引用类型的变量在进行比较时,比较的是他们的存储空间,所以得到的结果必然是false
&emsp;而List<T>Contains()方法是调用Equals()方法进行的比较,所以在对List<List<T>>使用Contains()方法查询一个List<T>是否存在于其中时,得到的结果也必然是false。
&emsp;C++中的vector重载了= =运算符,两个vector在使用= =运算符进行比较时,会将其中的元素从头到尾进行比较,即元素和元素的顺序都相同时,得到的结果就是1(true),否则是0(false)。
&emsp;在对vector<vector<T>>使用find方法查找其中是否存在某个vector时,find方法是使用= =运算符对每个元素进行比较,所以可以得到正确的结果。

&emsp;顺便扩展一下C++中的begin()和end()方法:

&emsp;使用前应该先引入容器头文件和命名空间std。

begin函数:返回指向指定接口参数所访问的集合开头的迭代器。
end函数:返回指向集合末尾以外的迭代器,该集合由指定的接口参数访问。

begin(myvector) 等同于执行 myvector.begin(),而 end(myvector) 也等同于执行 myvector.end()

参考资料:
List.Contains(T) 方法
vector类
C++ find()函数用法详解
C++ STL begin()和end()函数用法