题目描述
题目分析
这个题目自己大概花了一个小时,虽然是一遍AC,但是速度有点慢,太长时间不写代码导致自己对代码不太敏感,写起来慢腾腾的。
看到这个的想法就是,要用栈来保存列表的迭代器,这样将孩子列表遍历完成后才能重新回到父亲列表中,如果栈为空当然就结束了。
首先是对函数调用的分析,会首先调用hasNext
再调用next
,这就要求我们如果把后移操作放到hasNext
中,那么栈顶就要指向当前位置之前,对于迭代器来说不容易操作,因此我们决定将后移操作放到next
函数中
首要要定义栈中保存的迭代器的含义:我的想法是该层列表当前指向元素,这应该是比较符合直觉的,然而就是这种符合直觉的做法导致我的代码要复杂许多。因为这种定义其实是不统一的,栈顶的迭代器是直接可以访问元素的,而下面的迭代器因为保存的是当前元素的位置,所以栈顶访问结束后弹出以后是不能直接访问元素的,必须要执行一次后移操作,这种对栈中元素处理的不统一性导致代码变得复杂:一旦弹出必须要执行一次后移操作。可能这里还不明白为什么变复杂了,我再梳理一下需要进行的步骤:
首先,我们必须在hasNext
函数中判断,当前栈顶迭代器需不需要弹出,如果不需要,就要判断当前栈顶迭代器指向的是不是一个整数,如果是,返回真即可,如果不是,需要将子列表的迭代器压栈进行访问,可是子列表有可能为空,因此不能直接访问子列表,同样要对新压入栈的迭代器判断需不需要弹出。如果不需要弹出就是正常操作:要么继续压栈,要么返回。但是如果需要弹出的话,还需要对父亲迭代器进行后移操作,后移以后首先要判断的同样是需不需要弹出。
可以总结一下:
- 每次后移操作后首先要做的是判断是否需要弹出
- 每次入栈操作以后要判断是否需要弹出
如果我们直接按照上面的顺序进行操作,将会写出比较复杂的代码(我就是这样),可以看到弹出操作比较频繁,我们可以将其剥离成一个函数
实现代码
class NestedIterator {
public:NestedIterator(vector<NestedInteger> &nestedList) {S.emplace(nestedList.cbegin(), nestedList.cend());}int next() {return (S.top().first++)->getInteger();}bool hasNext() {return !is_empty() && move_to_integer();}
private:using vec_iter = vector<NestedInteger>::const_iterator;stack<pair<vec_iter, vec_iter>> S;bool is_empty() {//检查列表是否为空,应该在每次执行++操作后进行检查//如果为空返回真if (S.empty()) return true;while(S.top().first == S.top().second) {//如果栈顶部的迭代器已经失效,则将其弹出S.pop();if (S.empty()) return true;++S.top().first;}return false;}bool move_to_integer() {//将迭代器指向整数,如果不是整数,则将当前迭代器压入栈//返回假表示后面没有整数,只有空列表while( !( S.top().first -> isInteger()) ) {auto &tp = S.top();//如果当前迭代器指向的不是整数,则得到其指向的列表const auto &lst = tp.first -> getList();if (lst.empty()) {//如果指向的列表为空,则不用处理该列表,直接跳过++tp.first;if (is_empty()) {//如果后面没有有效元素,直接返回假return false;}} else {//如果列表不为空,则将列表的迭代器压栈S.emplace(lst.cbegin(), lst.cend());}}return true;}
};
写代码遇到一个小问题就是我将声明语句放到了表达式中,但是总报错,经过测试发现声明语句不能写在表达式中。
AC之后我又去看了一下官方题解,发现思路大同小异,但是发现人家的代码十分简洁,主要的区别在于hasNext
函数中题解通过将上面几个函数放到同一个循环里面,通过安排代码的顺序来完成任务:
- 先判断是否需要弹出,如果需要弹出则弹出后直接
continue
判断是否非空,然后再次执行循环,这时候就体现出设计的重要性了,题解中栈保存的是下次应该访问的迭代器地址,这就要求父列表迭代器在压入子列表迭代器后同时向后移动一位,因此弹出栈顶迭代器后就不需要再次向后移动了。如果不这样做,弹出后需要先判断栈是否为空,然后再后移,然后再判断是否需要弹出,将导致代码复杂 - 弹出后判断是否是一个整数,如果是直接返回
true
,如果不是则不断压栈,直到是一个整数。如果是一个空列表,则重新执行循环以后会弹出,因此不需要做多余的处理。
题解代码
class NestedIterator {
private:// pair 中存储的是列表的当前遍历位置,以及一个尾后迭代器用于判断是否遍历到了列表末尾stack<pair<vector<NestedInteger>::iterator, vector<NestedInteger>::iterator>> stk;public:NestedIterator(vector<NestedInteger> &nestedList) {stk.emplace(nestedList.begin(), nestedList.end());}int next() {// 由于保证调用 next 之前会调用 hasNext,直接返回栈顶列表的当前元素,然后迭代器指向下一个元素return stk.top().first++->getInteger();}bool hasNext() {while (!stk.empty()) {auto &p = stk.top();if (p.first == p.second) { // 遍历到当前列表末尾,出栈stk.pop();continue;}if (p.first->isInteger()) {return true;}// 若当前元素为列表,则将其入栈,且迭代器指向下一个元素auto &lst = p.first++->getList();stk.emplace(lst.begin(), lst.end());}return false;}
};
只能感叹题解代码的巧妙,自己距离这样的水平还很远,要多写多思考。这种模拟题感觉对代码能力还是挺有帮助的,因为工程中代码更多是这样的,注重的是代码的设计效率、是否优雅,而不是算法是否巧妙