Cracking the Coding Interview 150题(二)
2015-04-07 02:22
411 查看
3、栈与队列
3.1 描述如何只用一个数组来实现三个栈。3.2 请设计一个栈,除
pop与
push方法,还支持
min方法,可返回栈元素中的最小值。
pop、
push和
min三个方法的时间复杂度必须为
O(1)。
3.3 设想有一堆盘子,堆太高可能会倒下来。因此,在现实生活中,盘子堆到一定高度时,我们就会另外堆一堆盘子。请实现数据结构
SetOfStacks,模拟这种行为。
SetOfStacks应该由多个栈组成,并且在前一个栈填满时新建一个栈。此外,
SetOfStacks.push()和
SetOfStacks.pop()应该与普通栈的操作方法相同(也就是说,pop()返回的值,应该跟只有一个栈时的情况一样)
进阶:实现一个
popAt(int index)方法,根据指定的子栈,执行 pop 操作。
3.4 在经典问题汉诺塔中,有3根柱子及N个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自底向上从大到小依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时有以下限制:
每次只能移动一个盘子
盘子只能从柱子顶端滑出移到下一根柱子
盘子只能叠在比它大的盘子上
请运用栈,编写程序将所有盘子从第一根柱子移到最后一根柱子。
3.5 实现一个
MyQueue类,该类用两个栈来实现一个队列。
3.6 编写程序,按升序对栈进行排序(即最大元素位于栈顶)。最多只能使用一个额外的栈存放临时数据,但不得将元素复制到别的数据结构中(如数组)。该栈支持如下操作:
push、
pop、
peek和
isEmpty。
3.7 有家动物收容所只收容狗与猫,且严格遵守“先进先出”的原则。在收养该收容所的动物时,收养人只能收养所有动物中“最老”(根据进入收容所的时间长短)的动物,或者,可以挑选猫或狗(同时必须收养此类动物中“最老”的)。换言之,收养人不能自由挑选想收养的对象。请创建适用于这个系统的数据结构,实现各种操作方法,比如
enqueue、
dequeueAny、
dequeueDog和
dequeueCat等。
4、树与图
4.1 实现一个函数,检查二叉树是否平衡。在这个问题中,平衡树的定义如下:任意一个结点,其两棵子树的高度差不超过1。4.2 给定有向图,设计一个算法,找出两个结点之间是否存在一条路径。
4.3 给定一个有序整数数组,元素各不相同且按升序排列,编写一个算法,创建一棵高度最小的二叉查找树。
4.4 给定一棵二叉树,设计一个算法,创建含有某一深度上所有结点的链表(比如,若一棵树的深度为D,则会创建出D个链表)。
4.5 实现一个函数,检查一棵二叉树是否为二叉查找树。
4.6 设计一个算法,找出二叉查找树中指定结点的“下一个”结点(即中序后继)。可以假定每个结点都含有指向父结点的连接。
4.7 设计并实现一个算法,找出二叉树中某两个结点的第一个共同祖先。不得将额外的结点储存在另外的数据结构中。注意:这不一定是二叉查找树。
4.8 你有两棵非常大的二叉树:T1,有几百万个结点;T2,有几百个结点。设计一个算法,判断T2是否为T1的子树。(如果T1有这么一个结点n,其子树与T2一模一样,则T2为T1的子树。也就是说,从结点n处把树砍断,得到的树与T2完全相同。)
4.9 给定一棵二叉树,其中每个结点都含有一个数值。设计一个算法,打印结点数值总和等于某个给定值的所有路径。注意,路径不一定非得从二叉树的根结点或叶结点开始或结束。
参考答案(C++)
3.1 描述如何只用一个数组来实现三个栈。这个问题的难易程度取决于每个栈是固定分割 还是 动态分割。
固定分割:也就是每个栈分配固定大小的空间。这是最简单的实现方法,但是效率不高,因为即使某个栈是空的,它的空间也不能被别的栈使用。下面是每个栈占数组1/3的实现代码:
class Stacks { private: static const int size = 100; // 每个栈的大小 int tops[3]; // 3个栈的栈顶指针 int arr[3*size]; // 共享的数组 int absTopOfStack(int flag); // 返回栈顶指针在数组中的绝对量 public: Stacks(); bool isEmpty(int flag); // flag用0,1,2分别表示对3个栈进行操作 void push(int value, int flag); int pop(int flag); int top(int flag); }; Stacks::Stacks() { for(int i=0; i<3; ++i) tops[i] = -1; } int Stacks::absTopOfStack(int flag) { return flag * size + tops[flag]; } bool Stacks::isEmpty(int flag) { return tops[flag] == -1; } void Stacks::push(int value, int flag) { if(tops[flag]+1 >= size) /*检查有无空闲空间*/ { std::cout << "Out of space.\n"; } else { ++tops[flag]; arr[absTopOfStack(flag)] = value; } } int Stacks::pop(int flag) { if(isEmpty(flag)) { std::cout << "Trying to pop an empty stack.\n"; exit(1); } int value = arr[absTopOfStack(flag)]; arr[absTopOfStack(flag)] = 0; /*清零*/ --tops[flag]; /*指针自减*/ return value; } int Stacks::top(int flag) { return arr[absTopOfStack(flag)]; }
动态分割:允许栈的大小灵活可变,要实现起来难度有点大。
思路一:我们可以先考虑用一个数组实现两个栈,思路很简单:分别用数组的两端作为两个栈的起点,向中间扩展,若两个栈中的元素总和不超过n,两个栈不会重叠。基于同样的想法,我们可以把第三个栈实现在数组的中部,当前两个栈中有一个满了(即将重叠第三个栈时),平移第三个栈以扩展栈空间。这种方法由于需要搬移元素所以效率不高。
思路二:链式栈。通过链表的方式来实现栈,如下图:
链式栈是在一个数组上实现多个栈(3个、4个、5个…)的通用解决方案。下面是示例代码:
struct Node { int key; // 存储关键字 int preIndex; // 记录上一个元素的位置 }; class Stacks { private: int top1, top2, top3; int array_size; // 数组的大小,即栈的最大容量 int current_ptr; // 下一个元素入栈的位置 Node* arr; public: Stacks(int size); ~Stacks(); bool isEmpty(int flag); // flag用0,1,2分别表示对3个栈进行操作 void push(int value, int flag); int pop(int flag); int top(int flag); }; Stacks::Stacks(int size):array_size(size), top1(-1),top2(-1),top3(-1),current_ptr(0) { arr = new Node[size]; } Stacks::~Stacks() { delete [] arr; } bool Stacks::isEmpty(int flag) { switch(flag) { case 0: return top1 == -1; case 1: return top2 == -1; case 2: return top3 == -1; default: cout << "Error flag of stack.\n"; exit(1); } } void Stacks::push(int value, int flag) { if(current_ptr == array_size) // 栈已满 { cout << "Stack is full.\n"; return; } else { arr[current_ptr].key = value; switch (flag) { case 0: arr[current_ptr].preIndex = top1; top1 = current_ptr; break; case 1: arr[current_ptr].preIndex = top2; top2 = current_ptr; break; case 2: arr[current_ptr].preIndex = top3; top3 = current_ptr; break; default: break; } ++current_ptr; } } int Stacks::pop(int flag) { if(isEmpty(flag)) { cout << "Trying to pop an empty stack.\n"; exit(1); } int value; switch (flag) { case 0: value = arr[top1].key; top1 = arr[top1].preIndex; break; case 1: value = arr[top2].key; top2 = arr[top2].preIndex; break; case 2: value = arr[top3].key; top3 = arr[top3].preIndex; break; default: break; } return value; } int Stacks::top(int flag) { switch (flag) { case 0: return arr[top1].key; case 1: return arr[top2].key; case 2: return arr[top3].key; default: break; } }
3.2 请设计一个栈,除pop与push方法,还支持min方法,可返回栈元素中的最小值。pop、push和min三个方法的时间复杂度必须为O(1)。
通常来说
pop和
push方法的时间复杂度就是O(1),关键是
min方法。
可能有人会想 在Stack类里添加一个int型的变量用来记录最小值。当新元素入栈时,比较新元素与最小值,若新元素更小则更新最小值,此时
push的时间效率是O(1);但是当 minValue 出栈时,我们需要遍历整个栈,找出新的最小值,此时
pop操作的时间效率就不符合O(1)的要求了。
思路一:记录每种状态下的最小值。通过给栈元素增加一个 min 字段,每个元素在入栈时记录当前状态下的最小值。这么一来,要找到最小值,直接查看栈顶元素的 min 就行了。
struct node { int value; int min; }; class Stack { private: std::stack<node> s; public: void push(int v); int pop(); int min(); }; /**********实现***********/ void Stack::push(int v) { node n; n.value = v; n.min = v < min() ? v : min(); s.push(n); } int Stack::pop() { if(s.empty()) { std::cout << "Trying to pop an empty stack.\n"; exit(1); } int top = s.top().value; s.pop(); return top; } int Stack::min() { if(s.empty()) return INT_MAX; else return s.top().min; }
思路二:利用辅助栈保存最小值。这种方法比思路一更节省空间一些 ———— 因为思路一中每个栈元素都要记录 min,而使用辅助栈,当入栈元素大于当前最小值时,不需要记录。
class Stack { private: std::stack<int> s; std::stack<int> min_s; // 辅助栈 public: void push(int v); int pop(); int min(); }; /**********实现***********/ void Stack::push(int v) { if(v <= min()) min_s.push(v); s.push(v); } int Stack::pop() { if(s.empty()) { std::cout << "Trying to pop an empty stack.\n"; exit(1); } int top = s.top(); s.pop(); if(top == min()) min_s.pop(); return top; } int Stack::min() { if(min_s.empty()) return INT_MAX; else return min_s.top(); }
3.3 设想有一堆盘子,堆太高可能会倒下来。因此,在现实生活中,盘子堆到一定高度时,我们就会另外堆一堆盘子。请实现数据结构SetOfStacks,模拟这种行为。SetOfStacks应该由多个栈组成,并且在前一个栈填满时新建一个栈。此外,SetOfStacks.push() 和 SetOfStacks.pop() 应该与普通栈的操作方法相同(也就是说,pop() 返回的值,应该跟只有一个栈时的情况一样)
根据题意,
SetOfStacks中应该有一个栈数组,而
push和
pop都是操作栈数组中的最后一个栈。入栈时若最后一个栈被填满,就需新建一个栈;出栈后若最后一个栈为空,就必须从栈数组中移除这个栈。
class SetOfStacks { private: vector<stack<int>> stacks; int capacity; // 一个栈的最大存储量 public: SetOfStacks(int cap); void push(int v); int pop(); }; /**********实现**********/ SetOfStacks::SetOfStacks(int cap) { this->capacity = cap; } void SetOfStacks::push(int v) { if(!stacks.empty() && stacks.back().size() < capacity) { stacks.back().push(v); } else { stack<int> s; // 必须新建一个栈 s.push(v); stacks.push_back(s); } } int SetOfStacks::pop() { if(stacks.empty()) { cout << "Trying to pop an empty stack.\n"; exit(1); } int value = stacks.back().top(); stacks.back().pop(); if(stacks.back().empty()) stacks.pop_back(); // 移除 return value; }
进阶:实现一个popAt(int index)方法,根据指定的子栈,执行 pop 操作。
设想当弹出 栈1 的栈顶元素时,我们需要移出 栈2 的栈底元素,并将其推到栈1中。随后,将栈3的栈底元素推入栈2,将栈4的栈底元素推入栈3,以此类推。
有人可能会说,没必要执行“推入”操作,有些栈不填满也可以啊!而且还降低了时间复杂度。但是若之后有人假定所有的栈(最后一个栈除外)都是填满的,就可能出现意想不到的 error!这个问题并没有“标准答案”,你应该跟面试官讨论各种做法的优劣。
3.4 在经典问题汉诺塔中,有3根柱子及N个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自底向上从大到小依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时有以下限制:
每次只能移动一个盘子
盘子只能从柱子顶端滑出移到下一根柱子
盘子只能叠在比它大的盘子上
请运用栈,编写程序将所有盘子从第一根柱子移到最后一根柱子。
首先我们从最简单的开始整理自己的思路:
当
n=1时,因为只有一个盘子,所以可以直接将盘子1从柱1移至柱3.
当
n=2时,可以这样将所有盘子从柱1移至柱3:
将盘子1从柱1移至柱2。
将盘子2从柱1移至柱3。
将盘子1从柱2移至柱3。
当
n=3时,可以这样将所有盘子从柱1移至柱3:
将上面两个盘子从柱1移至柱2,同上。
将盘子3移至柱3。
将盘子1、2从柱2移至柱3。
当
n=4时,可以这样将所有盘子从柱1移至柱3:
将盘子1、2、3移至柱2,具体做法参见前面。
将盘子4移至柱3。
将盘子1、2、3移至柱3。
把柱1上的盘子移至柱3,需要柱2作为缓冲。可以看出,上面的过程是递归的,很自然地就可以导出递归算法。
class Tower { private: stack<int> disks; // 用整数的大小表示盘子的大小 public: void add(int d); // 向柱子上添加盘子 void moveButtomTo(Tower &t); // 移动最下面那块盘子 void moveDisks(int n, Tower &dest, Tower &buf); // 利用buf将n块盘子移至dest }; /*******************实现*********************/ void Tower::add(int d) { if(!disks.empty() && disks.top() <= d) { cout << "Error placing disk " << d; } else { disks.push(d); } } void Tower::moveButtomTo(Tower &t) { int top = disks.top(); disks.pop(); t.add(top); } // 递归实现 —— 注意使用引用 void Tower::moveDisks(int n, Tower &dest, Tower &buf) { if(n>0) { /*将上面的n-1块盘子移至缓冲区*/ moveDisks(n-1, buf, dest); /*将最下面那块盘子移至目的地*/ moveButtomTo(dest); /*将缓冲区的n-1块盘子移至目的地*/ buf.moveDisks(n-1, dest, *this); } } /*******************测试*********************/ int main() { Tower tower[3]; // 3根柱子 for(int i=5; i>0; --i) tower[0].add(i); // 移动 tower[0].moveDisks(5, tower[2], tower[1]); return 0; }
3.5 实现一个MyQueue类,该类用两个栈来实现一个队列。
队列和栈的主要区别就是元素进出顺序。假设两个栈分别是 Newest 和 Oldest,为了用这两个栈达到先进先出(FIFO)的效果,在入队时我们将元素压入 Newest 栈,然后将 Newest 的元素弹出,压入 Oldest 栈中(这样就达到了反转的效果),在出队时,我们从 Oldest 栈中弹出元素。
注意,为了避免频繁的执行从 Newest 到 Oldest 的反转操作,我们规定:只有在发现 Oldest 为空时,才执行反转操作 —— 将 Newest 中的所有元素弹出并压入 Oldest 中。
class MyQueue { private: stack<int> Newest; // 新入队的元素 stack<int> Oldest; // 准备出队的元素 void reverseStacks(); // 将Newest元素弹出,压入Oldest public: int size(); // 队列大小 void enqueue(int v); // 入队 int dequeue(); // 出队 int top(); // 队首元素 }; // Oldest为空才进行反转,避免频繁操作 void MyQueue::reverseStacks() { if(Oldest.empty()) { while(!Newest.empty()) { Oldest.push(Newest.top()); Newest.pop(); } } } int MyQueue::size() { return Oldest.size()+Newest.size(); } // 压入Newest,最新元素始终位于它的顶端 void MyQueue::enqueue(int v) { Newest.push(v); } // 从Oldest出队 int MyQueue::dequeue() { reverseStacks(); int value = Oldest.top(); Oldest.pop(); return value; } int MyQueue::top() { reverseStacks(); return Oldest.top(); }
3.6 编写程序,按升序对栈进行排序(即最大元素位于栈顶)。最多只能使用一个额外的栈存放临时数据,但不得将元素复制到别的数据结构中(如数组)。该栈支持如下操作:push、pop、peek和isEmpty。
可以想到的一种做法是,搜索整个栈,找出最小元素,将其压入另一个栈;然后,在剩余元素中找出最小的,并将其入栈。但这种做法实际上需要两个额外的栈,一个用来存放最终的有序序列,一个在搜索时用作缓冲区。
那么,只使用一个额外的栈怎么做呢?可以从S1逐一弹出元素,然后按顺序插入S2中,如下图所示:
S1是未排序的,S2是排好序的:
从S1中弹出5,我们需要在S2中找到合适的位置插入这个数,所以将 12 和 8 移至 S1 中,然后将 5 压入 S2。
那么 8 和 12 需不需要移回 S2 呢?其实不需要,对于这两个数,我们可以像处理 5 那样重复相关步骤就可以了。
stack<int> Sort(stack<int> s) { stack<int> r; while(!s.empty()) { int tmp = s.top(); s.pop(); // 弹出元素存到临时变量 while(!r.empty() && r.top() > tmp) { s.push(r.top()); r.pop(); } r.push(tmp); } return r; }
3.7 有家动物收容所只收容狗与猫,且严格遵守“先进先出”的原则。在收养该收容所的动物时,收养人只能收养所有动物中“最老”(根据进入收容所的时间长短)的动物,或者,可以挑选猫或狗(同时必须收养此类动物中“最老”的)。换言之,收养人不能自由挑选想收养的对象。请创建适用于这个系统的数据结构,实现各种操作方法,比如 enqueue、dequeueAny、dequeueDog 和 dequeueCat 等。
思路一:只维护一个队列。那么 dequeueAny 就容易实现,而 dequeueDog 和 dequeueCat 就需迭代访问整个队列,找到第一只被收养的狗或猫。这种解法明显效率不高。
思路二:为猫和狗各维护一个队列。那么 dequeueDog 和 dequeueCat 很容易实现,而 dequeueAny 需要比较猫队列与狗队列的队首,看哪个“更老”。为了方便 dequeueAny 的实现,我们给每个动物加一个额外的变量,以标记进入队列的先后顺序。这种解法显然更简单更高效!
class Animal { public: string name; int order; // 标记先后顺序 Animal(string s):name(s){} }; /******* 狗 *******/ class Dog : public Animal { public: Dog(string s):Animal(s){} }; /******* 猫 *******/ class Cat : public Animal { public: Cat(string s):Animal(s){} }; /*******队列*******/ class Queue { private: list<Dog> dogs; list<Cat> cats; int order; public: Queue():order(0){} void enqueue(Dog d) { d.order = order++; dogs.push_back(d); } void enqueue(Cat c) // 重载 { c.order = order++; cats.push_back(c); } Dog dequeueDog() { Dog d = dogs.front(); dogs.pop_front(); return d; } Cat dequeueCat() { Cat c = cats.front(); cats.pop_front(); return c; } Animal dequeueAny() { if(dogs.size() == 0) return dequeueCat(); if(cats.size() == 0) return dequeueDog(); if(dogs.front().order < cats.front().order) return dequeueDog(); else return dequeueCat(); } };
下面的题是关于树或图,做下面的题之前,首先我们要能够创建一棵二叉树或一个图:
创建二叉树:二叉树是什么相信就不用我多说了,可以递归地根据输入创建一棵二叉树。
typedef struct TreeNode { int data; TreeNode* left; TreeNode* right; } *BiTree; // 递归地创建二叉树 void createBinaryTree(BiTree &T) { int x; cin >> x; if(x < 0) { T = NULL; return; } T = (TreeNode*)malloc(sizeof(TreeNode)); T->data = x; createBinaryTree(T->left); createBinaryTree(T->right); } int main() { BiTree T; createBinaryTree(T); getchar(); return 0; }
创建二叉查找树: 可以由一个数组生成一棵二叉查找树,见《二叉查找树(BST)》。
创建图:图有两种存储方式,邻接矩阵和邻接表,这里采用邻接表来创建图。
class Graph { public: int V; // 顶点数 list<int> *adj; // 邻接表 Graph(int V); // 构造函数 void addEdge(int v, int w); // 向图中添加边 }; /* 构造函数 */ Graph::Graph(int V) { this->V = V; adj = new list<int>[V]; } /* 添加边,构造邻接表 */ void Graph::addEdge(int v, int w) { adj[v].push_back(w); // 将w添加到v的链表 }
4.1 实现一个函数,检查二叉树是否平衡。在这个问题中,平衡树的定义如下:任意一个结点,其两棵子树的高度差不超过1。
本题明确地给出了平衡树的定义,我们的解法就是根据定义直接递归检查每棵子树的高度。代码中的 checkHeight 方法以递归方式获取左右子树的高度。若子树是平衡的,返回该子树的实际高度;若子树不平衡,返回-1,这时所有递归都会立即返回:
/* * 平衡返回高度,不平衡返回-1 */ int checkHeight(BiTree T) { if(T == NULL) return 0; /* 检查左子树是否平衡 */ int leftHeight = checkHeight(T->left); if(leftHeight == -1) return -1; /* 检查右子树是否平衡 */ int rightHeight = checkHeight(T->right); if(rightHeight == -1) return -1; /* 检查当前结点是否平衡 */ int diff = leftHeight>rightHeight ? leftHeight-rightHeight : rightHeight-leftHeight; if(diff > 1) // 不平衡,返回-1 return -1; else // 平衡,返回高度 return leftHeight>rightHeight ? leftHeight+1 : rightHeight+1; } bool isBalance(BiTree T) { if(checkHeight(T) == -1) return false; else return true; }
4.2 给定有向图,设计一个算法,找出两个结点之间是否存在一条路径。
只需通过图的遍历,比如深度优先搜索或广度优先搜索,就能解决这个问题。
我们从其中一个结点出发,在遍历过程中检查是否找到另一个结点。在这个算法中,访问过的结点都应标记为“已访问”,以免循环和重复访问结点。下面的示例代码使用了广度优先搜索:
bool isPathExist(Graph g, int start, int end) { list<int> queue; // 当做队列 int V = g.getVertexNum(); // 顶点个数 bool *visited = new bool[V]; for(int i=0; i<V; ++i) visited[i] = false; visited[start] = true; // 将当前顶点标记为已访问并压入队列 queue.push_back(start); list<int>::iterator i; int node; while(!queue.empty()) { node = queue.front(); // 出队 queue.pop_front(); for(i=g.adj[node].begin(); i!=g.adj[node].end(); ++i) { if(!visited[*i]) { if(*i == end) // 是否等于另一个结点 return true; else { visited[*i] = true; queue.push_back(*i); } } } } return false; }
4.3 给定一个有序整数数组,元素各不相同且按升序排列,编写一个算法,创建一棵高度最小的二叉查找树。
要让二叉查找树的高度最小,就必须让左右子树的结点数越接近越好。根据二叉查找树的性质(中序遍历的序列是一个递增的有序序列),可以让该数组中间的值成为根节点,前半区间成为左子树,后半区间成为右子树。然后,每一个区间中间的值又成为子树的根节点,以此类推。
TreeNode* createMinBST(int A[], int low, int high) { if(low > high) /*递归终止条件*/ return NULL; int mid = (low + high)/2; TreeNode* T = (TreeNode*)malloc(sizeof(TreeNode)); T->data = A[mid]; T->left = createMinBST(A, low, mid-1); T->right = createMinBST(A, mid+1, high); return T; }
4.4 给定一棵二叉树,设计一个算法,创建含有某一深度上所有结点的链表(比如,若一棵树的深度为D,则会创建出D个链表)。
根据题意,你可能认为这个问题需要一层一层遍历,每一层构成一个链表。但其实可以用任意方式遍历树,只要记住结点位于哪一层即可。
下面是使用先序遍历实现的一个例子:
void createLevelLists(BiTree T, vector<list<TreeNode*>> &lists, int level) { if(T == NULL) /*递归终止条件*/ return; if(lists.size() <= level) { list<TreeNode*> lst; lst.push_back(T); lists.push_back(lst); } else { lists.at(level).push_back(T); } createLevelLists(T->left, lists, level+1); // 左子树 createLevelLists(T->right, lists, level+1); // 右子树 }
当然,你也可以使用其他遍历方式,比如层序遍历、广度优先搜索。
4.5 实现一个函数,检查一棵二叉树是否为二叉查找树。
思路一:检查中序序列是否是升序。这是二叉查找树的性质,但需要注意的是,这种方法无法正确处理树中的重复值。若假定这棵树不包含重复值,则这种方法是有效的。
bool inOrder(BiTree T, int &last) { if(T == NULL) /*递归终止条件*/ return true; /*检查左子树*/ if(!inOrder(T->left, last)) return false; /*检查当前结点*/ if(T->data <= last) return false; last = T->data; /*检查右子树*/ if(!inOrder(T->right, last)) return false; return true; } bool checkBST(BiTree T) { int last = INT_MIN; return inOrder(T, last); }
思路二:自上而下传递最小和最大值,判断每个结点是否在范围内。假定根结点的值是20,最开始的范围是(
INT_MIN,
INT_MAX),根结点明显在这个范围内。然后判断左孩子是否在(
INT_MIN, 20)这个范围内,右孩子是否在(20 ,
INT_MAX)这个范围内。以此类推,递归下去。
bool checkBST(BiTree T, int min, int max) { if(T == NULL) /*递归终止条件*/ return true; if(T->data <= min || T->data > max) return false; if(!checkBST(T->left,min,T->data) || !checkBST(T->right,T->data,max)) return false; return true; } bool checkBST(BiTree T) { return checkBST(T, INT_MIN, INT_MAX); }
4.6 设计一个算法,找出二叉查找树中指定结点的“下一个”结点(即中序后继)。可以假定每个结点都含有指向父结点的连接。
见《BST的前驱与后继》,本题要求的是中序遍历中的后继结点。求一个结点 x 的后继,有两种情况:
若结点 x 的右子树不为空,则 x 的后继是右子树中值最小的结点,即右子树最左边的结点。
若结点 x 的右子树为空,表示已遍访 x 的子树。我们必须回到 x 的父结点,记父结点为 p :
如果 x 是 p 的左儿子,那么下一个要访问的结点就是 p ;
如果 x 是 p 的右儿子,表示已遍访 p 的子树,这时需从 p 往上继续访问,直到遇到一个祖先结点 pp,它的左儿子也是结点 x 的祖先。
TreeNode* successor_BST(TreeNode* n) { if(n == NULL) return NULL; if(n->right != NULL) { TreeNode* tmp = n->right; while(tmp->left!=NULL) { tmp = tmp->left; } return tmp; } else { TreeNode* p = n->parent; while(p!=NULL && p->right==n) { n = p; p = p->parent; } return p; } }
4.7 设计并实现一个算法,找出二叉树中某两个结点的第一个共同祖先。不得将额外的结点储存在另外的数据结构中。注意:这不一定是二叉查找树。
我们在解题之前应该先要问问面试官,这棵树的结点是否包含指向父结点的指针。
情况一:如果每个结点中包含指向父结点的指针,那么就可以直接向上追踪 p 和 q 的路径,直到两者相交。当然,在向上追踪的过程中我们需要标记结点是否已经被访问过,比如可以给结点添加
isVisited域、或者将已访问结点映射到散列表。
情况二:如果结点不包含指向父结点的指针,又不得将额外的结点储存在另外的数据结构中。那么我们的做法就是:从上向下判断,若 p 和 q 都在某结点的左边,就到左子树中查找共同祖先;若都在该结点的右边,则在右子树中查找共同祖先。要是 p 和 q 不在同一边,那么就表示已经找到第一个共同祖先了。
// 若p为root的子孙,则返回true bool cover(TreeNode* root, TreeNode* p) { if(root == NULL) return false; if(root == p) return true; return cover(root->left, p) || cover(root->right, p); } TreeNode* getCommonAncester(BiTree T, TreeNode* p, TreeNode* q) { if(T == NULL) return NULL; if(T == p || T == q) return T; bool pAtLeft = cover(T->left, p); bool qAtLeft = cover(T->left, q); /*若p和q不在同一边,则表示已经找到第一个共同祖先*/ if(pAtLeft != qAtLeft) return T; /*若在同一边,遍访那一边*/ TreeNode* child = pAtLeft ? T->left : T->right; return getCommonAncester(child, p, q); } TreeNode* commonAncester(BiTree T, TreeNode* p, TreeNode* q) { if(!cover(T, p) || !cover(T, q)) // --错误检查-- return NULL; return getCommonAncester(T, p, q); }
4.8 你有两棵非常大的二叉树:T1,有几百万个结点;T2,有几百个结点。设计一个算法,判断T2是否为T1的子树。(如果T1有这么一个结点n,其子树与T2一模一样,则T2为T1的子树。也就是说,从结点n处把树砍断,得到的树与T2完全相同。)
首先考虑小数据量的情况,可以求出两棵树的前序和中序遍历序列,若 T2 前序遍历是 T1 前序遍历的子串,并且 T2 中序遍历是 T1 中序遍历的子串,则 T2 为 T1 的子树。假设T1的节点数为 N,T2的节点数为 M。遍历两棵树的时间复杂度是 O(N + M), 判断字符串是否为另一个字符串的子串的复杂性也是 O(N + M)(比如使用KMP算法)。所以总的时间复杂度是
O(N+M),所需的空间也是
O(N+M)。———— 这里需要注意一点:对于左结点或者右结点为 null 的情况,需要在字符串中插入特殊字符表示。
对于简单的情形,上面的解法还算不错。但是当数据量非常大时,暂存前序和中序序列可能要占用太多的内存,所以我们考虑另一种解法:遍历 T1,每当 T1 的某个节点与 T2 的根节点值相同时,就判断两棵子树是否相同。假设 T2 的根节点在 T1 中出现了 k 次,那么算法的时间复杂度就是
O(N + k*M),最坏情况下是
O(N*M)。
// 匹配两棵子树,完全一样返回true bool matchTree(TreeNode* t1, TreeNode* t2) { if(t1 == NULL && t2 == NULL) /*若两者都为空*/ return true; if(t1 == NULL || t2 == NULL) /*若只有一个为空*/ return false; if(t1->data != t2->data) return false; return matchTree(t1->left,t2->left) && matchTree(t1->right,t2->right); } // 遍历大树t1,当某个结点与t2根结点相同,matchTree判断 bool subTree(BiTree t1, BiTree t2) { if(t1 == NULL) return false; /*大的树已经空了,还未找到子树*/ if(t1->data == t2->data) { if(matchTree(t1, t2)) return true; } return subTree(t1->left, t2) || subTree(t1->right, t2); } bool containTree(BiTree t1, BiTree t2) { if(t2 == NULL) /*空树一定是子树*/ return true; return subTree(t1, t2); }
对于上面的两种解法,哪种解法比较好呢?
方法一会占用 O(N + M) 的内存,而另外一种解法只会占用 O(logN + logM) 的内存(递归的栈内存)。当考虑扩展性时,内存使用的多寡是个很重要的因素。
方法一的时间复杂度为O(N + M),方法二最差的时间复杂度是O(N*M)。但是最差情况的时间复杂度并没有代表性,我们需要进一步观察,因为更可能的情况是很早就发现两棵树的不同,早早的退出了 matchTree。
总的来说,在空间效率上,第二种解法更好。在时间上,需要通过实际数据来验证。
4.9 给定一棵二叉树,其中每个结点都含有一个数值。设计一个算法,打印结点数值总和等于某个给定值的所有路径。注意,路径不一定非得从二叉树的根结点或叶结点开始或结束。
下面我们采用简化推广法来解题。
Step 1 : 简化——假设路径必须从根节点开始,但可以在任意结点结束,该怎么解决?
在这种情况下,问题就会变得容易很多。我们可以从根节点开始,向下访问子节点,计算每条路径上到当前节点为止的数值总和,若与给定值相同则打印当前路径。注意,就算找到总和,仍要继续访问这条路径(因为可能存在正负相抵消的情况)。
Step 2 : 推广——路径可从任意结点开始
如果路径可以从任意结点开始,在任意结点结束。在这种情况下我们稍作调整,对于每个结点,都向“上”检查是否有总和为 sum 的路径。具体来讲就是:递归访问每个结点 p 时,我们将 root 到 p 的完整 path 传入函数;然后,函数会从 p 到 root 逆序将结点上的值加起来,当每条子路径的总和等于 sum 时,打印该条子路径。
代码如下:
// 打印从start到end的路径 void print(int path[], int start, int end) { for(int i=start; i<=end; ++i) cout << path[i] << " "; cout << endl; } // 求一棵子树的高度 int depth(TreeNode* n) { if(n == NULL) return 0; else { int leftDepth = depth(n->left); int rightDepth = depth(n->right); return leftDepth>rightDepth ? leftDepth+1 : rightDepth+1; } } void findSum(BiTree T, int sum, int path[], int level) { if(T == NULL) return; /*将当前结点插入路径*/ path[level] = T->data; /*从当前结点到root结点,看是否存在和为sum的路径*/ int t = 0; for(int i=level; i>=0; --i) { t += path[i]; if(t == sum) print(path, i, level); } /*递归*/ findSum(T->left, sum, path, level+1); findSum(T->right, sum, path, level+1); } void findSum(BiTree T, int sum) { int dep = depth(T); int *path = (int*)malloc(dep*sizeof(int)); findSum(T, sum, path, 0); free(path);/*释放内存*/ }
个人站点:http://songlee24.github.com
相关文章推荐
- Cracking the coding interview 150 要点记录(一)--Array and List
- { Cracking The Coding Interview: 150 programming Q&A } --- Arrays and Strings
- Cracking the Coding Interview 150题(二)
- Cracking the Coding Interview 150题(一)
- Cracking the Coding Interview 150题(一)
- { Cracking The Coding Interview: 150 programming Q&A } 5th edition Part I
- { Cracking The Coding Interview: 150 programming Q&A } 5th edition Part II
- Cracking the coding interview--Q2.2
- 《Cracking the Coding Interview》——第8章:面向对象设计——题目6
- 《Cracking the Coding Interview》——第12章:测试——题目5·
- 《Cracking the Coding Interview》——第13章:C和C++——题目10
- 《Cracking the Coding Interview》——第16章:线程与锁——题目2
- 《Cracking the Coding Interview》——第17章:普通题——题目8
- 《Cracking the Coding Interview》——第18章:难题——题目6
- Cracking The Coding Interview 3rd -- 1.4
- Cracking the Coding Interview 6.2
- Cracking the coding interview--Q9.3
- Cracking the coding interview--Q2.5
- Cracking the coding interview--Q4.4
- Cracking the coding interview--Q5.2