第8章 数据结构3

算法的设计依赖于数据的逻辑结构,算法的实现依赖于数据的存储结构,所以数据结构选择得好环,对程序的质量影响甚大。掌握基本的数据结构知识,是程序设计水平提高的必要条件。算法和数据结构也是面试中的必考题型。

 面试题21  编程实现队列的入队、出队、测长、打印
 考点;队列的各项基本操作 出现频率:★★★★
 【解析】
 队列的实现可使用链表和数组,本题中我们使用单链表来实现队列。因此我们构造一个如图8.5所示结构的队列。

根据上面的结构图,我们可以使用下面的结构体定义队列和队列的节点。

typedef struct _Node
{
	int data;
	struct _Node* next;		//指向植表下一个指针
}node;

typedef struct _Queue
{
	node* front;			//队头
	node* rear;				//队尾
}MyQueue;

其中,node表示队列中的每个节点元素,MyQueue表示队列。
在进行本题要求的编程之前,我们首先要构造一个空的队列,代码如下。

//构造空的队列
MyQueue* CreateMyQueue()
{
	MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));
	q->front = nullptr;			//把队首指针置空
	q->rear = nullptr;			//把队尾指针置空
	return q;
}

CreateMyQueue函数中,把front和rear指针都置为nullptr。 下面进行各个函数的编程。
 1.入队

 //入队,从队尾一端插入节点
MyQueue* enqueue(MyQueue* q, int data)
{
	node* newP = nullptr;
	newP = (node*)malloc(sizeof(node));		//新建节点
	newP->data = data;						//复制节点数据
	newP->next = nullptr;
	if (q->rear == nullptr)
	{
		//若队列为空,则新节点既是队首又是队尾
		q->front = q->rear = newP;
	}
	else
	{
		//若队列不为空,则新节点放到队尾,队尾指针指向新节点
		q->rear->next = newP;
		q->rear = newP;
	}
	return q;
}

由队列的特点可知,入队操作是在队尾一端进行插入的,enqueue函数的说明如下。
 口 代码第5~7行,根据数据data新建一个数据节点。
 口 代码第8~12行,如果队列为空,把新建的节点作为对首,同时也作为队尾。注 意:此时不仅要操作rear指针,同时也需要对队列的front指针进行操作。
 □代码第13~18行,如果队列不为空,把新建的节点连接到队尾,并将它作为新的 队尾,此时只需要对front指针进行操作。
 2.出队

//出队,从队头一端删除节点
MyQueue* dequeue(MyQueue* q)
{
	node* pnode = nullptr;
	pnode = q->front;					//指向队头
	if (pnode == nullptr)				//队列为空
	{
		printf("Empty queue!\n");
	}
	else
	{
		q->front = q->front->next;		//新队头
		if (q->front == nullptr)		//当翻除后队列为空时,对rear置空
		{
			q->rear = nullptr;
		}
		free(pnode);					//测除原队头节点
	}
	return q;
}

出队与入队的操作不同,它是在队首一端进行删除的,dequeue函数的说明如下。
 口代码第5行,指针pnode指向队头节点,也就是即将要删除的指针节点。
 口代码第6~9行,如果队列为空,打印“Empty queue”消息并返回。
 口 代码第12~18行,把队头节点往后移,然后删除原来的对头节点。如果删除后的 队列为空,则把rear指针置空。
 
 这里为简单起见,dequeue函数删除节点的同时释放了节点内存。如果要得到被删除的 节点,可以不释放内存并且返回此节点。

 3.测长

//队列的测长
int GetLength(MyQueue* q) 
{
	int nlen = 0;
	node* pnode = q->front;				//指向队头
	if (pnode != nullptr)				//队列不为空
	{
		nlen = 1;
	}
	while (pnode != q->rear)			//遍历队列
	{
		pnode = pnode->next;
		nlen++;							//循环一次,nlen递增1
	}
	return nlen;
}

 GetLength函数的实现很简单,只需要遍历一次队列中的节点就可以获得。只有一点需
 要注意,在代码第10行中,循环结束的条件是pnode !=q->rear,而不应该与NULL做比较 (pnode !=NULL)。这是因为队尾有可能指向的不是一个链表的末节点。

 4.打印

//队列的打印
void PrintMyQueue(MyQueue* q)
{
	node* pnode = q->front;
	if (pnode == nullptr)				//队列为空
	{
		printf("Empty Queue! \n");
		return;
	}
	printf("data: ");
	while (pnode != q->rear)			//遍历队列
	{
		printf("%d ", pnode->data);		//打印节点数据
		pnode = pnode->next;
	}
	printf("%d ", pnode->data);			//打印队尾节点数据
}

PrintMyQueue函数的实现也很简单。与GetLength函数一样,都需要进行一次遍历队列中的节点,而且循环结束的条件也是pnode !=q->rear,最后打印队尾节点数据(代码第 16行)。
 下面是对上面各个函数的简单测试。

int main()
{
	int nlen = 0;
	MyQueue* hp = CreateMyQueue();		//建立队列
	enqueue(hp, 1);						//入队 1 2 3 4
	enqueue(hp, 2);
	enqueue(hp, 3);
	enqueue(hp, 4);
	nlen = GetLength(hp);				//获得队列长度
	printf("nlen = %d\n", nlen);
	PrintMyQueue(hp);					//打印队列数据
	dequeue(hp);						//出队两次
	dequeue(hp);
	nlen = GetLength(hp);				//再次获得队列长度
	printf("\nnlen = %d\n", nlen);
	PrintMyQueue(hp);					//再次打印队列数据
	return 0;
}

执行结果如下。

 面试题22    队列和栈有什么区别
 考点:队列和栈的区别
 出现频率:★★★★★
 【解析】
 队列与栈是两种不同的数据结构。它们有以下区别。
 (1)操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进 栈,栈的删除称为出栈。
 (2)可操作的方向不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进 栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
 (3)操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则 进行的。新来的成员总是加入队尾(不能中间插入),每次离开的成员总是队列头上的 (不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中“最 新的”元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后 才能删除。

 面试题23  简答题——队列和栈的使用
 考点:队列和栈的使用 出现频率:★★★★★
 一个顺序为1,2,3,4,5,6的栈,依次进入一个队列,然后进栈,顺序是什么?
 【解析】
 首先一个顺序为1,2,3,4,5,6的栈,其意思是进栈的顺序是1,2,3,4,5,6。 按照栈的结构,1由于最先进栈,所以被放入栈底:6最后进栈,因此6位于栈顶。
 然后进入一个队列。因为只能在栈顶进行出栈操作,也就是说,6最先出栈,1最后出 栈。因此,队列的入队顺序(也就是栈的出栈顺序)为6,5,4,3,2,1。
 最后再进栈。队列是个FIFO(先进先出)的结构,因此出队顺序与入队的顺序相同,即
 6,5,4,3,2,1。也就是6最先进栈,1最后进栈。因此,此时6位于栈底,1位于栈顶。
 【答案】
 最后的入栈顺序为6,5,4,3,2,1。

面试题24  选择题——队列和栈的区别
 考点:队列和栈的区别
 出现频率:★★★★
 
 Which one is correc accordingto Stack and Queue?
 A.Stack is FILO,while Queue is FIFO
 B.Stack is FIFO,while Queue is FILO
 C.Both Stack and Queue are FILO
 D.Both Stackand Queue are FIFO
 【解析】
 本题考查的是栈与队列的区别,栈是先进后出(FILO),而队列是先进先出(FIFO)。
 【答案】
 A

 面试题25   使用队列实现栈
 考点:队列的使用以及栈的实现 出现频率:★★★★ 编程实现下面的stack,并根据这个stack完成queue的操作。

 class MyStack
 {
 	void push(data);
 	void pop(&data);
 	bool isEmpty();
 };

【解析】
 首先说明这个stack的实现。显然,我们需要实现栈的3种基本操作,即进栈、出栈以 及判空。为方便起见,这里使用单链表结构实现栈并且使用类的形式来定义栈及其节点。 首先是节点类和栈类的具体定义,源代码如下。

class MyData
{
public:
	MyData():data(0), next(nullptr) { }					//默认构造函数
	MyData(int value):data(value), next(nullptr) { }	//带参数的构造函数
	int data;
	MyData* next;
};

class MyStack
{
public:
	MyStack(): top(nullptr) { }							//默认构造函数
	void push(MyData data);								//进栈
	void pop(MyData* pData);							//出栈
	bool IsEmpty();										//是否为空模
	MyData* top;										//栈顶
};

 这里MyData定义了单链表的节点,其中data表示节点的数据域,next指向下一个节点。 MyStack表示栈的定义,其中private成员top表示栈顶,由于不能直接操作栈底,因此这里 没有定义栈底的指针。在MyStack的默认构造函数中,把栈顶指针top置空,表示此时栈为 空栈。
 接下来是进栈、出栈以及判空的代码实现。

 //进栈
void MyStack::push(MyData data)
{
	MyData* pData = nullptr;
	pData = new MyData(data.data);				//生成新节点
	pData->next = top;							//与原来的栈顶节点相连
	top = pData;								//栈顶节点为新加入的节点
}

//出栈,返回栈顶节点内容
void MyStack::pop(MyData* data)
{
	if (IsEmpty())								//如果栈为空,则直接返回
	{
		return;
	}
	data->data = top->data;						//给传出的参数赋值
	MyData* p = top;							//临时保存原栈顶节点
	top = top->next;							//移动栈顶,指向下一个节点
	delete p;									//释放原栈顶节点内存
}

MyStack:push函数表示进栈操作,它实际上就是在单链表的首部进行插入操作,并且 top一直指向这个单链表的首部。
 MyStack:pop函数表示出栈操作,它实际上就是在单链表的首部进行删除操作,并且 top一直指向这个单链表的首部。另外,它还把删除节点的数据保存到data参数中(代码第 17行)。
 MyStack::IsEmpty函数非常简单。当空栈时,top指针为NULL:而当栈中有节点时, top指向链表头,因此只需要用top与NULL进行比较即可。
 下面是栈操作的测试代码。

int main()
{
	MyData data(0);									//定义一个节点
	MyStack s;										//定义一个栈结构
	s.push(MyData(1));								//进栈三次:1,2,3
	s.push(MyData(2));
	s.push(MyData(3));

	s.pop(&data);									//第1次出栈
	cout << "pop " << data.data << endl;
	s.pop(&data);									//第2次出栈
	cout << "pop " << data.data << endl;
	s.pop(&data);									//第3次出栈
	cout << "pop " << data.data << endl;
	cout << "Empty = " << s.IsEmpty() << endl;		//打印判空

	return 0;
}

执行结果为:

 可以看出,进栈的顺序和出栈的顺序相反。
 在前面的小节里,我们已经实现过queue,当时我们用了front和rear两个指针分别指 向队头和队尾。这里由于题目限制,我们不能使用这些指针。
 如何只使用stack实现queue呢?我们知道stack是先进后出的(FILO),而queue是先 进先出的(FIFO)。也就是说,stack进行了一次反向。如果进行两次反向,就能实现queue 的功能,所以我们需要两个stack实现queue。
 下面是具体的思路。
 假设有两个栈A和B,且都为空。可以认为栈A为提供入队列的功能,栈B提供出队 列的功能。
 (1)如果栈B不为空,直接弹出栈B的数据。
 (2)如果栈B为空,则依次弹出栈A的数据,放入栈B中,再弹出栈B的数据。 于是,我们可以得到下面的MyQueue定义及实现。
 

class MyQueue
{
public:
	void enqueue(MyData data);			//入队
	void dequeue(MyData& data);			//出队
	bool IsEmpty();						//是否为空队
private:
	MyStack s1;							//用于入队
	MyStack s2;							//用于出队
};

//入队
void MyQueue::enqueue(MyData data)
{
	s1.push(data);						//只对s1进行进栈操作
}

//出队
void MyQueue::dequeue(MyData& data)
{
	MyData temp(0);			//局部变量,用于临时存储
	if (s2.IsEmpty())
	{
		//如果s2为空,把s1的所有元素push到s2中
		while (!s1.IsEmpty())
		{
			s1.pop(&temp);		//弹出s1的元素
			s2.push(temp);		//压入s2中
		}
	}
	if (!s2.IsEmpty())
	{
		//此时如果s2不为空,则弹出s2的栈顶元素
		s2.pop(&data);
	}
}

//队列判空
bool MyQueue::IsEmpty()
{
	//如果两个栈都为空,则返回1.否则返回0
	return (s1.IsEmpty() && s2.IsEmpty());
}

测试MyQueue的程序如下。

int main()
{
	//测试队列
	MyData data(0);		//定义一个节点
	MyQueue q;
	q.enqueue(MyData(1));
	q.enqueue(MyData(2));
	q.enqueue(MyData(3));

	q.dequeue(data);
	cout << "dequeue " << data.data << endl;
	q.dequeue(data);
	cout << "dequeue " << data.data << endl;
	q.dequeue(data);
	cout << "dequeue " << data.data << endl;
	cout << "IsEmpty: " << q.IsEmpty() << endl;

	return 0;
}

执行结果:

 面试题26   选择题——栈的使用
 考点:队列和栈的区别
 出现频率:★★★★
 设栈的最大长度为3,入栈序列为1,2,3,4,5,6,则不可能得出的栈序列是:
 A.1.2.3.4,5,6
 B.2,1,3,4,5.6
 C.3.4,2,1,5,6
 D.4,3.2,1,5.6
 【解析】
 此题隐含了一个前提,就是任何时候都能进栈和出栈。 下面我们具体分析每一个选项。
 选项A:顺序为1,2,3,4,5,6。很明显,如果每次有一个元素进栈,然后马上出 栈,即1进栈,1出栈,2进栈,2出栈,如此进行就可以遵循这个顺序。
 选项B:顺序为2,1,3,4,5,6。先入栈1,2,然后全部出栈,即2,1出栈。接 着后面的元素和选项A的方式一样,有一个元素进栈,然后马上出栈,这样也可以遵循 这个顺序。
 选项C:顺序为3,4,2,1,5,6。先入栈1,2,3,然后做一次出栈,即3出栈。接 着4进栈,并且马上全部出栈,即4,2,1出栈。后面的5,6的入栈、出栈方式和选项A 的一样,这样也可以遵循这个顺序。
 选项D:顺序为4,3,2,1,5,6。注意,这里的前面四个元素是4,3,2,1。如果 栈的最大长度为4,是可以实现按这个顺序出栈的。然而栈的最大长度为3,即元素4无法 和1,2,3同时存在栈内。所以无法按4,3,2,1顺序出栈。
 【答案】
 D

面试题27 用C++实现一个二叉排序树
 用C++实现一个二叉排序树,完成创建、插入节点,删除节点,查找节点功能。 考点:二叉排序树的各项基本操作
 出现频率:★★★★
 【解析】
 二叉排序树(Binary Sort Tree)又称二叉查找树(Binary Search Tree)。其定义为:二叉排序树或者是空树,或者是满足如下性质的二叉树。
 (1)若它的左子树非空,则左子树上所有节点的值均小于根节点的值。
 (2)若它的右子树非空,则右子树上所有节点的值均大于根节点的值。
 (3)左、右子树本身又各是一棵二叉排序树。
 上述性质简称二叉排序树性质(BST性质),故二叉排序树实际上是满足BST性质的二 叉树。
 图8.6就是一个简单的二叉排序树。

首先我们需要定义节点类以及二叉排序树类。对于节点类来说,显然每个节点有向下 的两个指针,而且每个节点都只有一个父节点。对于二叉排序树类来说,只需要有数的根 节点,就可以进行操作了。因此它们的数据结构如下。

//节点类定义
class Node
{
public:
	int data;				//数据
	Node* parent;			//父亲节点
	Node* left;			//左子节点
	Node* right;			//右子节点
public:
	Node(): data(-1), parent(nullptr), left(nullptr), right(nullptr){ }
	Node(int num) : data(num), parent(nullptr), left(nullptr), right(nullptr) { }
};

//二叉排序树类定义
class Tree
{
public:
	Tree(int num[], int len);						//插入num数组的前len个数据
	void insertNode1(int data);						//插入节点,递归方法
	void insertNode(int data);						//插入节点,非递归方法
	Node* searchNode(int data);						//查找节点
	void deleteNode(int data);						//删除节点及其子树
private:
	void insertNode(Node* current, int data);		//递归插入方法
	Node* searchNode(Node* current, int data);		//递归查找方法
	void deleteNode(Node* current);					//递归删除方法
private:
	Node* root;										//二叉排序树的根节点
};

第18行的构造函数定义了根据int数组创建二叉排序树的方法。 第19~20行定义两种插入节点的方法,分别为递归和非递归方法。 第21~22行分别定义了查找和删除节点的方法,这两个方法均为递归方法。
 另外注意,第24~26行中有3个private方法。这是因为,出于封装性的考虑,指针root 是私有成员变量,于是3个使用递归方法的函数(代码第20~22行)都只能有一个参数data。 然而,这里只含有一个参数data的函数是无法进行递归运算的。因此它们内部直接调用了这 3个private的对应方法,而这3个private方法直接使用递归。
 接下来具体说明各个方法的实现。
 1.创建二叉排序树的方法(构造函数中)
 这里可以首先生成根节点,然后循环调用插入节点的方法对二叉树进行插入操作。

 //插入num数组的前len个数据
Tree::Tree(int num[], int len)
{
	root = new Node(num[0]);		//建立root节点
	//把数组中的其他数组插入到二叉排序树中
	for (int i = 1; i < len; i++)
	{
		//insertNode(num[i]);
		insertNode1(num[i]);
	}
}

2.插入节点操作
 通常在二叉树的遍历中可以选择递归与非递归的方法。由于篇幅有限,本题中仅给出 插入时的这两种方法。
 nsertNodeI()使用非递归方法插入节点,其代码如下。

//插入数据为参数data的节点,非递归方法
void Tree::insertNode1(int data)		//插入节点
{
	Node* p, * par;
	Node* newNode = new Node(data);		//创建节点

	p = par = root;
	while (p != nullptr)				//查找插入在哪个节点下面
	{
		par = p;
		if (data > p->data)				//如果data大于当前节点的data
			p = p->right;				//下一步到左子节点,否则进行到右子节点
		else if (data < p->data)
			p = p->left;
		else if (data == p->data)		//不能插入重复数据
		{
			delete newNode;
			return;
		}
	}
	newNode->parent = par;
	if (par->data > newNode->data)		//把新节点插入在目标节点的正确位置
		par->left = newNode;
	else
		par->right = newNode;
}

它分为下面的步骤。
 (1)创建节点(代码第5行)。
 (2)查找新建节点的插入位置。
 根据前面介绍过的排序二叉树的性质,我们进行节点的比较(代码第11~14行)进行 节点的遍历。如果二叉树中已经含有相同数据的节点,则不予插入(代码第15~19行)且 直接返回。循环完毕后,p为NULL,par指向目标节点。
 (3)把新节点插入在目标节点的正确位置。与(2)相同,需要考虑到排序二叉树的 性质。

 insertNode()使用非递归的方法插入节点,它的内部调用了private函数insertNode()。代 码如下。

//插入数据为参数data的节点,调用递归插入方法
void Tree::insertNode(int data)
{
	if (root != nullptr)
	{
		insertNode(root, data);			//调用递归插入方法
	}
}

//递归插入方法
void Tree::insertNode(Node* current, int data)
{
	//如果data小于当前节点数据,则在当前节点的左子树插入
	if (data < current->data)
	{
		if (current->left == nullptr)			//如果左节点不存在,则插入到左节点
		{
			current->left = new Node(data);
			current->left->parent = current;
		}
		else
			insertNode(current->left, data);	//否则对左节点进行递归调用
	}

	//如果data大于当前节点数据,则在当前节点的右子树插入
	else if (data > current->data)
	{
		if (current->right == nullptr)			//如果右节点不存在,则插入到右节点
		{
			current->right = new Node(data);
			current->right->parent = current;
		}
		else
			insertNode(current->right, data);	//否则对右节点进行递归调用
	}

	return;										//data等于当前节点数据时,不播入
}

 现在说明private成员函数insertNode的步骤。
 (1)如果当前节点的数据值小于data,则应该在它的左子树插入。此时如果当前节点没 有左子节点,则直接插入新节点作为它的左子节点。否则把它的左子节点作为参数,再进 行整个过程。 (2)如果当前节点的数据值大于data,则应该在它的右子树插入。此时如果当前节点没有右子节点,则直接插入新节点作为它的右子节点。否则把它的右子节点作为参数,再进 行整个过程。
 比较这两个版本的插入函数可以看出,使用递归方法写出的程序简单,容易理解; 而非递归方法使用循环,与递归方法的程序结构相比显得臃肿。递归时需要不断地进 行函数入栈、出栈操作,因此效率方面较低:并且如果递归的深度较大,可能会引发栈 溢出。
 查找节点也使用了递归,由于与插入节点类似,这里只列出private方法。

//递归查找方法
Node* Tree::searchNode(int data)
{
	Node* current = nullptr;
	if (root != nullptr)
	{
		current = searchNode(root, data);			//调用递归插入方法
	}

	return current;
}

Node* Tree::searchNode(Node* current, int data)
{
	//如果data小于当前节点数据,则递归搜索其左子树
	if (data < current->data)
	{
		if (current->left == nullptr)				//如果不存在左子树,则返回NULL
			return nullptr;
		return searchNode(current->left, data);		
	}

	//如果data大于当前节点数据,则递归搜索其左子树
	else if (data > current->data)
	{
		if (current->right == nullptr)				//如果不存在右子树,则返回NULL
			return nullptr;
		return searchNode(current->right, data);
	}

	//如果相等,则返回current
	return current;
}

searchNode的步骤如下。
 (1)如果data小于当前节点(current)的值,且current的左子树存在,则继续搜索current 的左子树,否则返回NULL。
 (2)如果data大于当前节点(current)的值,且current的右子树存在,则继续搜索current 的左子树,否则返回NULL。 (3)如果data等于当前节点(current)的值,则返回current。
 3.删除节点
 删除节点的操作代码如下。

//删除数据为data的节点及其子树
void Tree::deleteNode(int data)
{
	Node* current = nullptr;

	current = searchNode(data);		//查找节点
	if (current != nullptr)
	{
		deleteNode(current);		//删除节点及其子树
	}
}

//删除current节点及其子树的所有节点
void Tree::deleteNode(Node* current)
{
	if (current->left != nullptr)		//删除左子树
		deleteNode(current->left);
	if (current->right != nullptr)		//删除右子树
		deleteNode(current->right);

	if (current->parent == nullptr)
	{
		//如果current是根节点,把root置空
		delete current;
		root = nullptr;
		return;
	}

	//将current父亲节点的相应指针置空
	if (current->parent->data > current->data) //current为其父节点的左子节点
		current->parent->left = nullptr;
	else		//current为parNode的右子节点
		current->parent->right = nullptr;

	//最后删此节点
	delete current;
}

public成员函数deleteNode()删除数据为data的节点及其子树。它首先调用了 searchNode()查找数据等于data的节点(代码第6行),如果找到节点,则调用private成员 函数deleteNode的节点及其子树。
 private成员函数也是使用递归方法进行删除操作的。它的步骤如下。
 (1)如果current左子树存在,则递归删除current左子树。 (2)如果current右子树存在,则递归删除current右子树。
 (3)最后删除current节点。此时如果current是根节点,则需要把root置空,否则把其 父节点相应的指针置空。

 面试题28  使用递归与非递归方法实现中序遍历
 考点:中序遍历算法的实现
 出现频率:★★★
 【解析】
 中序遍历的递归算法定义为,若二叉树非空,则依次执行如下操作。
 (1)遍历左子树;
 (2)访问根节点;
 (3)遍历右子树。
 中序遍历的递归算法程序代码如下。

//中序遍历
void Tree::InOrderTree()
{
	if (root == nullptr)
		return;
	InOrderTree(root);
}

void Tree::InOrderTree(Node* current)
{
	if (current != nullptr)
	{
		InOrderTree(current->left);			//遍历左子树
		cout << current->data << " ";		//打印节点数据
		InOrderTree(current->right);		//遍历右子树
	}
}

对于中序遍历非递归算法,这里使用一个栈(stack)来临时存储节点,方法如下。 (1)先将根节点入栈,遍历左子树。
 (2)遍历完左子树返回时,栈顶元素应为根节点,此时出栈,并打印节点数据。 (3)再中序遍历右子树。
 代码如下。

void Tree::InOrderTreeUnRec()
{
	stack<Node*> s;
	Node* p = root;
	while (p != nullptr || !s.empty())
	{
		while (p != nullptr)			//遍历左子树
		{
			s.push(p);					//把遍历的节点全部压栈
			p = p->left;
		}

		if (!s.empty())
		{
			p = s.top();				//得到模顶内容
			s.pop();					//出栈
			cout << p->data << " ";		//打印
			p = p->right;				//指向右子节点,下一次循环时就会中序遍历右子树
		}
	}
}

main函数测试如下。

int main()
{
	int num[] = { 5, 3, 7, 2, 4, 6, 8, 1 };
	Tree tree(num, 8);
	cout << "InOrder: ";
	tree.InOrderTree();					//中序遍历,递归方法
	cout << "\nInOrder: ";
	tree.InOrderTreeUnRec();			//中序遍历,非递归方法
	return 0;
}

测试结果为

另外,从这个结果可以看出,中序遍历的结果就是二叉排序树的排序结果。

 面试题  29使用递归与非递归方法实现先序遍历
 考点:先序遍历算法的实现
 出现频率:★★★
 【解析】
 先序遍历的递归算法定义为,若二叉树非空,则依次执行如下操作。
 (1)访问根结点;
 (2)遍历左子树;
 (3)遍历右子树。
 先序遍历的程序代码如下。

//先序遍历
void Tree::PreOrderTree()
{
	if (root == nullptr)
		return;
	PreOrderTree(root);
}

void Tree::PreOrderTree(Node* current)
{
	if (current != nullptr)
	{
		cout << current->data << " ";	//打印节点数据
		PreOrderTree(current->left);	//遍历左子树
		PreOrderTree(current->right);	//遍历右子树
	}
}

对于先序遍历非递归算法,这里我们使用一个栈(stack)来临时存储节点,方法如下。 (1)打印根节点数据。
 (2)把根节点的right入栈,遍历左子树。 (3)遍历完左子树返回时,栈顶元素应为right,出栈,遍历以该指针为根的子树。
 程序如下

void Tree::PreOrderTreeUnRec()
{
	stack<Node*> s;
	Node* p = root;
	while (p != nullptr || !s.empty())
	{
		while (p != nullptr)				//遍历左子树
		{
			cout << p->data << " ";			//打印
			s.push(p);						//把遍历的节点全部压栈
			p = p->left;
		}

		if (!s.empty())
		{
			p = s.top();					//得到栈顶内容
			s.pop();						//出栈
			p = p->right;					//指向右子节点,下一次循环时就会先序遍历左子树
		}
	}
}


 main函数测试如下。

int main()
{
	int num[] = { 5, 3, 7, 2, 4, 6, 8, 1 };
	Tree tree(num, 8);
	cout << "PreOrder: ";
	tree.PreOrderTree();				//先序遍历,递归方法
	cout << "\nPreOrder: ";
	tree.PreOrderTreeUnRec();			//先序遍历,非递归方法
	return 0;
}

 测试结果为:

面试题30  使用递归与非递归方法实现后序遍历
 考点:后序遍历算法的实现
 出现频率:★★★
 【解析】
 后序遍历的递归算法定义为,若二叉树非空,则依次执行如下操作。
 (1)遍历左子树;
 (2)遍历右子树;
 (3)访问根结点。
 后序遍历的程序代码如下。

//后序遍历
void Tree::PostOrderTree()
{
	if (root == nullptr)
		return;
	PostOrderTree(root);
}

void Tree::PostOrderTree(Node* current)
{
	if (current != nullptr)
	{
		PostOrderTree(current->left);		//遍历左子树
		PostOrderTree(current->right);		//遍历右子树
		cout << current->data << " ";		//打印节点数据
	}
}

现在说明对于后序遍历的非递归方法。假设T是要遍历树的根指针,后序遍历要求在 遍历完左、右子树后再访问根。需要判断根结点的左、右子树是否均遍历过。
 可采用标记法,结点入栈时,配一个标志tag.一同入栈(tag为0表示遍历左子树前的 现场保护,tag为1表示遍历右子树前的现场保护)。
 首先将T和tag(为0)入栈,遍历左子树:返回后,修改栈顶tag为1,遍历右子树: 最后访问根结点。代码如下。

void Tree::PostOrderTreeUnRec()
{
	stack<Node*> s;
	Node* p = root;

	while (p != nullptr || !s.empty())
	{
		while (p != nullptr)
		{
			s.push(p);						//压栈
			p = p->left;					//遍历左子树
		}

		if (!s.empty())
		{
			p = s.top();					//得到栈顶元素
			if (p->tag)						//tag为1时
			{
				cout << p->data << " ";		//打印节点数据
				s.pop();					//出栈
				p = nullptr;				//第二次访问标志其右子树已经遍历
			}
			else
			{
				p->tag = 1;					//修改tag为1
				p = p->right;				//指向右节点,下次遍历其左子树
			}
		}
	}
}

根据上面的分析,代码中Node类添加了一个int型的成员变量tag。main函数测试如下。

int main()
{
	int num[] = { 5, 3, 7, 2, 4, 6, 8, 1 };
	Tree tree(num, 8);
	cout << "PostOrder: ";
	tree.PostOrderTree();				//后序遍历,递归方法
	cout << "\nPostOrder: ";
	tree.PostOrderTreeUnRec();			//后序遍历,非递归方法
	return 0;
}

测试结果为:

面试题31    编写层次遍历二叉树的算法
 考点:层次遍历算法的实现
 出现频率:★★
 【解析】
 层次遍历就是一层一层地进行遍历。比如图8.6共有4层,各层分别为
第1层:5
第2层:3、7
第3层:2、4、6、8
第4层:1
本题很难直接用节点的指针(left、right、parent)来实现,但是借助一个队列就可以轻 松地实现,例如图8.7所示的二叉树结构。
可以按照下面的方式执行。
 (1)A入队CX。
 (2)A出队,同时A的子节点B、C入队(此时队列有B、C)。
(3)B出队,同时B的子节点D、E入队(此时队列有C、D、E)。
(4)C出队,同时C的子节点F、G入队(此时队列有D、E、F、G)。
显然,这样就能使出队的顺序满足层次遍历。

这里为了方便,我们选择使用C++标准库中的queue模板类。代码如下。

//层次遍历
void Tree::LevelOrderTree()
{
	queue<Node*> q;							//定义队列q,它的元素为Node*类型指针
	Node* ptr = nullptr;

	q.push(root);							//根节点入队
	while (!q.empty())
	{
		ptr = q.front();					//得到队头节点
		q.pop();							//出队
		cout << ptr->data << " ";
		if (ptr->left != nullptr)			//当前节点存在左节点,则左节点入队
		{
			q.push(ptr->left);
		}
		if (ptr->right != nullptr)			//当前节点存在右节点,则右节点入队
		{
			q.push(ptr->right);
		}
	}
}

程序中对队列q进行入队和出队的操作。每次出队时,就打印此出队的元素,并且对 这个元素节点的左子节点和右子节点进行入队。
测试代码如下。

int main()
{
	int num[] = { 5, 3, 7, 2, 4, 6, 8, 1 };
	Tree tree(num, 8);
	cout << "LevelOrder: ";
	tree.LevelOrderTree();				//层次遍历
	return 0;
}

执行结果如下。

面试题32  编写判别给定二叉树是否为二叉排序树的算法
 考点:二叉排序树判定算法的实现
 出现频率:★★★
 【解析】
 在前面的面试题中,我们已经编写过中序遍历的算法。我们知道,使用中序遍历的结果就 是排序二叉树的排序输出,因此我们可以使用中序遍历来实现判定二叉树是否为二叉排序树。 代码如下:

//使用中序遍历判断二又树是否为排序二又树
bool IsSortedTree(Tree tree)
{
	int lastvalue = 0;
	stack<Node*> s;
	Node* p = tree.root;
	while (p != nullptr || !s.empty())
	{
		while (p != nullptr)		//遍历左子树
		{
			s.push(p);				//把遍历的节点全部压栈
			p = p->left;
		}

		if (!s.empty())
		{
			p = s.top();			//得到栈顶内容	
			s.pop();				//出栈
			if (lastvalue == 0 || lastvalue < p->data)
			{
				//如果是第一次弹出或者lastvalue小于当前节点值,给lastvalue赋值
				lastvalue = p->data;
			}
			else if (lastvalue >= p->data)
			{
				//如果lastvalue大于当前节点值,则返回false
				return false;
			}

			p = p->right;		//指向右子节点,下一次循环时就会中序遍历右子树
		}
	}

	return true;				//到这里说明节点数据是升序排列,返回true
}

这里我们使用了非递归的二叉树遍历方式,其中使用了一个lastvalue来记录上一次出 队的节点数据。如果出现lastvalue大于或等于当前出队的节点值,则返回false;否则一直 入队和出队。如果lastvalue始终小于当前出队的节点值,则是升序排列,返回true。

下面是主函数的测试代码。

int main(void)
{
	int num[] = { 5,3 ,7,2,4,6,8,1 };
	Tree tree(num, 8);
	cout << "InOrder:";
	tree.InOrderTreeUnRec(); 		//中序遍历,非递归方法 
	cout << "\nIsSortedTree: " << IsSortedTree(tree) << endl;
	Node* node = tree.searchNode(4);
	node->data = 1; 			//手动把节点的数据改成1
	cout << "InOrder: ";
	tree.InOrderTreeUnRec();		//中序遍历,非递归方法
	cout << "\nIsSortedTree: " << IsSortedTree(tree) << endl;
	return 0;
}

测试结果:

27-32 所有源码

#include <iostream>
#include <stack>
#include <queue>

using namespace std;

//节点类定义
class Node
{
public:
	int data;				//数据
	Node* parent;			//父亲节点
	Node* left;			//左子节点
	Node* right;			//右子节点
	int tag{ 0 };
public:
	Node(): data(-1), parent(nullptr), left(nullptr), right(nullptr){ }
	Node(int num) : data(num), parent(nullptr), left(nullptr), right(nullptr) { }
};

//二叉排序树类定义
class Tree
{
public:
	Tree(int num[], int len);						//插入num数组的前len个数据
	void insertNode1(int data);						//插入节点,递归方法
	void insertNode(int data);						//插入节点,非递归方法
	Node* searchNode(int data);						//查找节点
	void deleteNode(int data);						//删除节点及其子树
	
	void InOrderTree();								//中序遍历
	void InOrderTree(Node* current);
	void InOrderTreeUnRec();
	void PreOrderTree();							//先序遍历
	void PreOrderTree(Node* current);
	void PreOrderTreeUnRec();
	void PostOrderTree();							//后序遍历
	void PostOrderTree(Node* current);
	void PostOrderTreeUnRec();

	//层次遍历
	void LevelOrderTree();

private:
	void insertNode(Node* current, int data);		//递归插入方法
	Node* searchNode(Node* current, int data);		//递归查找方法
	void deleteNode(Node* current);					//递归删除方法
public:
	Node* root;										//二叉排序树的根节点
};

//插入num数组的前len个数据
Tree::Tree(int num[], int len)
{
	root = new Node(num[0]);		//建立root节点
	//把数组中的其他数组插入到二叉排序树中
	for (int i = 1; i < len; i++)
	{
		//insertNode(num[i]);
		insertNode1(num[i]);
	}
}

//插入数据为参数data的节点,非递归方法
void Tree::insertNode1(int data)		//插入节点
{
	Node* p, * par;
	Node* newNode = new Node(data);		//创建节点

	p = par = root;
	while (p != nullptr)				//查找插入在哪个节点下面
	{
		par = p;
		if (data > p->data)				//如果data大于当前节点的data
			p = p->right;				//下一步到左子节点,否则进行到右子节点
		else if (data < p->data)
			p = p->left;
		else if (data == p->data)		//不能插入重复数据
		{
			delete newNode;
			return;
		}
	}
	newNode->parent = par;
	if (par->data > newNode->data)		//把新节点插入在目标节点的正确位置
		par->left = newNode;
	else
		par->right = newNode;
}

//插入数据为参数data的节点,调用递归插入方法
void Tree::insertNode(int data)
{
	if (root != nullptr)
	{
		insertNode(root, data);			//调用递归插入方法
	}
}

//递归插入方法
void Tree::insertNode(Node* current, int data)
{
	//如果data小于当前节点数据,则在当前节点的左子树插入
	if (data < current->data)
	{
		if (current->left == nullptr)			//如果左节点不存在,则插入到左节点
		{
			current->left = new Node(data);
			current->left->parent = current;
		}
		else
			insertNode(current->left, data);	//否则对左节点进行递归调用
	}

	//如果data大于当前节点数据,则在当前节点的右子树插入
	else if (data > current->data)
	{
		if (current->right == nullptr)			//如果右节点不存在,则插入到右节点
		{
			current->right = new Node(data);
			current->right->parent = current;
		}
		else
			insertNode(current->right, data);	//否则对右节点进行递归调用
	}

	return;										//data等于当前节点数据时,不播入
}

Node* Tree::searchNode(int data)
{
	Node* current = nullptr;
	if (root != nullptr)
	{
		current = searchNode(root, data);			//调用递归插入方法
	}

	return current;
}

//递归查找方法
Node* Tree::searchNode(Node* current, int data)
{
	//如果data小于当前节点数据,则递归搜索其左子树
	if (data < current->data)
	{
		if (current->left == nullptr)				//如果不存在左子树,则返回NULL
			return nullptr;
		return searchNode(current->left, data);		
	}

	//如果data大于当前节点数据,则递归搜索其左子树
	else if (data > current->data)
	{
		if (current->right == nullptr)				//如果不存在右子树,则返回NULL
			return nullptr;
		return searchNode(current->right, data);
	}

	//如果相等,则返回current
	return current;
}

//删除数据为data的节点及其子树
void Tree::deleteNode(int data)
{
	Node* current = nullptr;

	current = searchNode(data);		//查找节点
	if (current != nullptr)
	{
		deleteNode(current);		//删除节点及其子树
	}
}

//删除current节点及其子树的所有节点
void Tree::deleteNode(Node* current)
{
	if (current->left != nullptr)		//删除左子树
		deleteNode(current->left);
	if (current->right != nullptr)		//删除右子树
		deleteNode(current->right);

	if (current->parent == nullptr)
	{
		//如果current是根节点,把root置空
		delete current;
		root = nullptr;
		return;
	}

	//将current父亲节点的相应指针置空
	if (current->parent->data > current->data) //current为其父节点的左子节点
		current->parent->left = nullptr;
	else		//current为parNode的右子节点
		current->parent->right = nullptr;

	//最后删此节点
	delete current;
}

//中序遍历
void Tree::InOrderTree()
{
	if (root == nullptr)
		return;
	InOrderTree(root);
}

void Tree::InOrderTree(Node* current)
{
	if (current != nullptr)
	{
		InOrderTree(current->left);			//遍历左子树
		cout << current->data << " ";		//打印节点数据
		InOrderTree(current->right);		//遍历右子树
	}
}

void Tree::InOrderTreeUnRec()
{
	stack<Node*> s;
	Node* p = root;
	while (p != nullptr || !s.empty())
	{
		while (p != nullptr)			//遍历左子树
		{
			s.push(p);					//把遍历的节点全部压栈
			p = p->left;
		}

		if (!s.empty())
		{
			p = s.top();				//得到模顶内容
			s.pop();					//出栈
			cout << p->data << " ";		//打印
			p = p->right;				//指向右子节点,下一次循环时就会中序遍历右子树
		}
	}
}
//28
//int main()
//{
//	int num[] = { 5, 3, 7, 2, 4, 6, 8, 1 };
//	Tree tree(num, 8);
//	cout << "InOrder: ";
//	tree.InOrderTree();					//中序遍历,递归方法
//	cout << "\nInOrder: ";
//	tree.InOrderTreeUnRec();			//中序遍历,非递归方法
//	return 0;
//}

//先序遍历
void Tree::PreOrderTree()
{
	if (root == nullptr)
		return;
	PreOrderTree(root);
}

void Tree::PreOrderTree(Node* current)
{
	if (current != nullptr)
	{
		cout << current->data << " ";	//打印节点数据
		PreOrderTree(current->left);	//遍历左子树
		PreOrderTree(current->right);	//遍历右子树
	}
}

void Tree::PreOrderTreeUnRec()
{
	stack<Node*> s;
	Node* p = root;
	while (p != nullptr || !s.empty())
	{
		while (p != nullptr)				//遍历左子树
		{
			cout << p->data << " ";			//打印
			s.push(p);						//把遍历的节点全部压栈
			p = p->left;
		}

		if (!s.empty())
		{
			p = s.top();					//得到栈顶内容
			s.pop();						//出栈
			p = p->right;					//指向右子节点,下一次循环时就会先序遍历左子树
		}
	}
}

//29
//int main()
//{
//	int num[] = { 5, 3, 7, 2, 4, 6, 8, 1 };
//	Tree tree(num, 8);
//	cout << "PreOrder: ";
//	tree.PreOrderTree();				//先序遍历,递归方法
//	cout << "\nPreOrder: ";
//	tree.PreOrderTreeUnRec();			//先序遍历,非递归方法
//	return 0;
//}

//后序遍历
void Tree::PostOrderTree()
{
	if (root == nullptr)
		return;
	PostOrderTree(root);
}

void Tree::PostOrderTree(Node* current)
{
	if (current != nullptr)
	{
		PostOrderTree(current->left);		//遍历左子树
		PostOrderTree(current->right);		//遍历右子树
		cout << current->data << " ";		//打印节点数据
	}
}

void Tree::PostOrderTreeUnRec()
{
	stack<Node*> s;
	Node* p = root;

	while (p != nullptr || !s.empty())
	{
		while (p != nullptr)
		{
			s.push(p);						//压栈
			p = p->left;					//遍历左子树
		}

		if (!s.empty())
		{
			p = s.top();					//得到栈顶元素
			if (p->tag)						//tag为1时
			{
				cout << p->data << " ";		//打印节点数据
				s.pop();					//出栈
				p = nullptr;				//第二次访问标志其右子树已经遍历
			}
			else
			{
				p->tag = 1;					//修改tag为1
				p = p->right;				//指向右节点,下次遍历其左子树
			}
		}
	}
}

//30
//int main()
//{
//	int num[] = { 5, 3, 7, 2, 4, 6, 8, 1 };
//	Tree tree(num, 8);
//	cout << "PostOrder: ";
//	tree.PostOrderTree();				//后序遍历,递归方法
//	cout << "\nPostOrder: ";
//	tree.PostOrderTreeUnRec();			//后序遍历,非递归方法
//	return 0;
//}

//层次遍历
void Tree::LevelOrderTree()
{
	queue<Node*> q;							//定义队列q,它的元素为Node*类型指针
	Node* ptr = nullptr;

	q.push(root);							//根节点入队
	while (!q.empty())
	{
		ptr = q.front();					//得到队头节点
		q.pop();							//出队
		cout << ptr->data << " ";
		if (ptr->left != nullptr)			//当前节点存在左节点,则左节点入队
		{
			q.push(ptr->left);
		}
		if (ptr->right != nullptr)			//当前节点存在右节点,则右节点入队
		{
			q.push(ptr->right);
		}
	}
}
//31
//int main()
//{
//	int num[] = { 5, 3, 7, 2, 4, 6, 8, 1 };
//	Tree tree(num, 8);
//	cout << "LevelOrder: ";
//	tree.LevelOrderTree();				//层次遍历
//	return 0;
//}

 
//使用中序遍历判断二又树是否为排序二又树
bool IsSortedTree(Tree tree)
{
	int lastvalue = 0;
	stack<Node*> s;
	Node* p = tree.root;
	while (p != nullptr || !s.empty())
	{
		while (p != nullptr)		//遍历左子树
		{
			s.push(p);				//把遍历的节点全部压栈
			p = p->left;
		}

		if (!s.empty())
		{
			p = s.top();			//得到栈顶内容	
			s.pop();				//出栈
			if (lastvalue == 0 || lastvalue < p->data)
			{
				//如果是第一次弹出或者lastvalue小于当前节点值,给lastvalue赋值
				lastvalue = p->data;
			}
			else if (lastvalue >= p->data)
			{
				//如果lastvalue大于当前节点值,则返回false
				return false;
			}

			p = p->right;		//指向右子节点,下一次循环时就会中序遍历右子树
		}
	}

	return true;				//到这里说明节点数据是升序排列,返回true
}

int main(void)
{
	int num[] = { 5,3 ,7,2,4,6,8,1 };
	Tree tree(num, 8);
	cout << "InOrder:";
	tree.InOrderTreeUnRec(); 		//中序遍历,非递归方法 
	cout << "\nIsSortedTree: " << IsSortedTree(tree) << endl;
	Node* node = tree.searchNode(4);
	node->data = 1; 			//手动把节点的数据改成1
	cout << "InOrder: ";
	tree.InOrderTreeUnRec();		//中序遍历,非递归方法
	cout << "\nIsSortedTree: " << IsSortedTree(tree) << endl;
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值