由力扣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;
}
 但是在对示例[1,2,2]进行测试时,得到的返回结果是这样的:
 可以看到,重复的列表[2]和[1,2]并没有被检测出来,即使他们与已有的列表中的数据和顺序都完全一致。当时我隐约觉得这可能和List是引用类型有关,从而导致在将两个List进行比较时无法得到我想要的结果。
 于是,为了赶在12点之前写完代码提交,又不想直接抄一份交上去,我选择了换成C++碰碰运气。
 根据以往的经验,力扣的题目在C#的方法返回值是IList时,C++的方法返回值应该是vector。果然,C++的模板如下:
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
}
};
 把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;
}
};
 对[1,2,2]这一示例进行测试发现得到的返回结果是正确的,猜对了(
 但是倒在了这个输入前面:
 检查发现应该是对于两个vector,如果其中的数据相同但是顺序不同,会被视作两个不同的vector,于是加入下面的代码来在每次将tempList存入ansList前,对tempList进行排序:
sort(tempList.begin(), tempList.end());
 然后终于通过了,虽然运行时间只击败了6%(
 那么今天就来研究一下为什么C#中的两个List即使包含的数据和顺序在都一致时会被判断为不相等,以及C++中两个vector为什么能被判断为相等。
对C#中的List的思考研究
 根据最初的代码内容,想要找到这个答案就应该从List<T>.Contains()方法入手。
 在查阅了微软的C#文档后,发现其中提到:
 The List
class uses both an equality comparer and an ordering comparer.
 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 IEquatablegeneric interface, then the equality comparer is the Equals(T) method of that interface; otherwise, the default equality comparer is Object.Equals(Object).
 官方机翻:
 List
类使用相等比较器和排序比较器。
 诸如Contains、IndexOf、LastIndexOf 和Remove等方法,对列表元素使用相等比较器。 按如下方式确定类型的默认相等比较器 T 。如果类型 T 实现 IEquatable泛型接口,则相等比较器是 Equals(T) 该接口的方法; 否则,默认的相等比较器为 Object.Equals(Object)。
 也就是说List如果继承了IEquatableList<T>.Equals(T)是如何实现的就行了。查看List的原码,发现List并没有继承IEquatableObject.Equals(Object)方法。
 对于这个方法,官方文档的描述是这样的:
如果指定的对象等于当前对象,则为 true;否则为 false。
 这句说了和没说一样的话并不能解决眼下的问题,不过根据以往的积累,如果进行比较的两个对象是值类型,那么比较的是两个对象的值;如果进行比较的两个对象是引用类型,那么比较的是两个对象的引用(也就是内存空间)。
 List类型当然不是值类型,那么两个List无论如何进行比较,得到的结果都必然是不相等。
 (为了严谨还是测试一下)

 结果是False

 结果是True
 到这里,两个List无法比较的问题已经解决了。
对C++中的vector的思考研究
 根据C++版本的代码中的内容,我们应该看看find()方法的实现。
 find() 函数本质上是一个模板函数,用于在指定范围内查找和目标元素值相等的第一个元素。
 如下为 find() 函数的语法格式:
InputIterator find (InputIterator first, InputIterator last, const T& val);
 其中,first 和 last 为输入迭代器,[first, last) 用于指定该函数的查找范围;val 为要查找的目标元素。
 正因为 first 和 last 的类型为输入迭代器,因此该函数适用于所有的序列式容器。
 另外,该函数会返回一个输入迭代器,当 find() 函数查找成功时,其指向的是在 [first, last) 区域内查找到的第一个目标元素;如果查找失败,则该迭代器的指向和 last 相同。
 值得一提的是,find() 函数的底层实现,其实就是用= =运算符将 val 和 [first, last) 区域内的元素逐个进行比对。这也就意味着,[first, last) 区域内的元素必须支持= =运算符。
(不是,两个= =中间怎么会标黄的啊(==这样==
 那么来看看vector有没有重载= =运算符
 根据 这篇博客 ,虽然微软的C++文档中没有写vector重载了= =运算符,但实际上vector是重载了= =运算符的。
*虽然卡文上没有写,但是这张怪兽可以特殊召唤((无端联想
 两个vector的比较,正如上文中说到的那样,在元素相同且元素排列顺序相同时,返回的结果为1,否则为0。
 测试:
 输出为1
 输出为0的比较懂得都懂,就不放图了。
总结
 C#中的List<T>没有继承IEquatable<T>接口,所以两个List在使用Equals()方法进行比较时,使用的是Object类的Equals()方法,两个没有重载Equals()方法的引用类型的变量在进行比较时,比较的是他们的存储空间,所以得到的结果必然是false。
 而List<T>的Contains()方法是调用Equals()方法进行的比较,所以在对List<List<T>>使用Contains()方法查询一个List<T>是否存在于其中时,得到的结果也必然是false。
 C++中的vector重载了= =运算符,两个vector在使用= =运算符进行比较时,会将其中的元素从头到尾进行比较,即元素和元素的顺序都相同时,得到的结果就是1(true),否则是0(false)。
 在对vector<vector<T>>使用find方法查找其中是否存在某个vector时,find方法是使用= =运算符对每个元素进行比较,所以可以得到正确的结果。
 顺便扩展一下C++中的begin()和end()方法:
 使用前应该先引入容器头文件和命名空间std。
begin函数:返回指向指定接口参数所访问的集合开头的迭代器。
end函数:返回指向集合末尾以外的迭代器,该集合由指定的接口参数访问。
begin(myvector) 等同于执行 myvector.begin(),而 end(myvector) 也等同于执行 myvector.end()
参考资料:
List
vector类
C++ find()函数用法详解
C++ STL begin()和end()函数用法