二叉树、二叉树排序树的实现及遍历

  在学习二叉树之前先要学【先导知识】树的概念和表示方法

目录

1、二叉树

2、二叉树的遍历

3、二叉排序树


 前言

对于大量的数据而言,链表的线性访问时间太慢,不宜使用。本章节将会介绍一种简单的数据结构:树(tree),其大部分操作的运行时间平均为O(logN)。在数据结构中树是非常有用的抽象概念,在本篇中我们将讨论二叉树的存储结构、二叉树的遍历和二叉排序树的实现,为后续平衡树以及高阶搜索树打下基础。

1、二叉树

二叉树(binary tree)是一棵每一个节点最多只能有两个孩子的树(度为2),当一棵树只有左孩子或者右孩子的最坏情况称为斜树

当一棵树的度只为0或者2,我们把这种树称为满二叉树;当一棵树的节点严格按照从上到下、从左到右的的次序依次排列,我们把这种树称为完全二叉树,根据定义,满二叉树是完全二叉树的一种特殊情况;如果我们用NULL或者其他不可能出现的关键字来补满所有的空节点,那么这种树称为扩展二叉树。

同时二叉树还有些性质在考试中经常考到,这里也总结一下:

  1. 在第 i  层的二叉树上最多有 2^(i-1) 个节点;
  2. 深度为 k 的二叉树有 2^k-1 个节点; 
  3. 假设度为0、1、2的节点分别是n0、n1、n2,由于一棵树中节点数是T,所以T = n0 + n2 + n2,而树枝(节点之间的连线)的数量是T - 1,所以T - 1 = 2 * n2 + n1,由这两个关系式可得:n0=n2+1;
  4. 具有 n 个节点的完全二叉树深度为( log2n)+1 向下取整;
  5. 由性质4可得:具有 n 个节点的完全二叉树节点按层次编号(从上到下、从左到右),如果 i = 1是根节点,那么对于任意一个节点它的双亲节点序号是 i / 2(i > 1)向下取整,如果当 2 * i > n 那么 节点 i 一定没有左孩子(也没有右孩子),2 * i + 1 > n 没有右孩子;

对于二叉树的实现,我们同样可以考虑使用顺序结构或者链式结构。使用顺序结构,因为查找迅速在比赛中可以用到,我们可以根据上述的性质5,计算出父节点和它孩子之间的下标关系;

而因为一棵二叉树最多有两个孩子,所以我们在使用链式结构的时候可以直接用指针指向它们。

/*二叉树节点声明*/
typedef struct TreeNode
{
	int data;
	struct TreeNode* left, *right;
}Node;

至于二叉树的实现十分简单,哪有空就往哪插;

Node* root = NULL;

void init(int key)
{
	root = (Node*)malloc(sizeof(Node));
	root->data = key;
	root->left = NULL;
	root->right = NULL;
}

Node* findNode(Node* node ,int parent)
{
	static Node* temp = NULL;
	if (node->data == parent)
	{
		temp = node;
	}
	if(node->left)
	{
		findNode(node->left, parent);
	}
	if (node->right)
	{
		findNode(node->right, parent);
	}
	return temp;
}

void createNode(int key, int parent)
{
	Node* temp = findNode(root, parent);
	if (temp == NULL)
	{
		printf("NOT FIND\n");
		return;
	}
	else
	{
		if (temp->left == NULL)
		{
			Node* newnode = (Node*)malloc(sizeof(Node));
			newnode->data = key;
			newnode->left = NULL;
			newnode->right = NULL;
			temp->left = newnode;
		}
		else if (temp->right == NULL)
		{
			Node* newnode = (Node*)malloc(sizeof(Node));
			newnode->data = key;
			newnode->left = NULL;
			newnode->right = NULL;
			temp->right = newnode;
		}
		else
		{
			//左右子树都存在
			printf("FULL\n");
			return;
		}
	}
}

2、二叉树的遍历

接下来我们重点介绍二叉树的遍历。

二叉树的遍历根据使用场景的不同分为:前序遍历、中序遍历、后序遍历及层序遍历;

  • 前序遍历:先访问根节点再访问左子树再访问右子树;

如上所示的二叉树中,前序遍历的顺序是:1 ->2 -> 4 -> 5 -> 3 -> 6 ;

具体步骤如下图所示,访问顺序(由先到后)  绿  红  蓝 

  • 中序遍历:先访问左子树,再访问根节点,再访问右子树;

如上所示的二叉树中,中序遍历的顺序是:4 ->2 -> 5 -> 1 -> 6 -> 3

  • 后序遍历:先访问左子树,再访问右子树,再访问根节点;

如上所示的二叉树中,后序遍历的顺序是:4 ->5 -> 2 -> 6 -> 3 -> 1

通过上述案例,我们可以得到一个重要性质:前序遍历的第一个一定是根,后序遍历的最后一个一定是根,那么通过前序和中序或者后序和中序就能推出树的抽象模型;

而用代码实现前中后序遍历,用递归实现是最简单不过的了:

//前序
void prev_order(Node* node)
{
	if (node == NULL)
	{
		return;
	}
	printf("%d", node->data);
	prev_order(node->left);
	prev_order(node->right);
}

//中序
void in_order(Node* node)
{
	if (node == NULL)
	{
		return;
	}
	in_order(node->left);
	printf("%d", node->data);
	in_order(node->right);
}

//后序
void past_order(Node* node)
{
	if (node == NULL)
	{
		return;
	}
	past_order(node->left);
	past_order(node->right);
	printf("%d", node->data);
}

至于层序遍历,它能直观地将树的每一层输出,当然想要这样做用简单的递归肯定是不行的,这里我们使用队列来实现:

 第一步:我们先把根节点入队;第二步:判断队列是否为空,如果不为空,就把队头出队,将它的孩子入队,如此循环,直到队列为空跳出;

 代码的话我这里偷个小懒,用C++中的STL实现的队列,不会的同学正常写队列就行了,思路是一样的;

//层序
void level_order(Node* node)
{
    //创建队列
	std::queue<int> q;
	if (node != NULL)
	{
		q.push(node->data);
	}
	while(!q.empty())
	{
		int temp = q.front();
		printf("%d ", temp);
		Node* tempNode = findNode(root, temp);
		q.pop();
		if (tempNode->left)
		{
			q.push(tempNode->left->data);
		}
		if (tempNode->right)
		{
			q.push(tempNode->right->data);
		}
	}
}

3、二叉排序树

二叉排序树又叫做二叉搜索树:如果一棵二叉树它的左子树不空,那么左子树的所有值都小于根节点;若右子树不为空,那么所有右子树的值都大于根节点,这就是二叉排序树(binary search tree)。

如上所示就是一棵二叉排序树。二叉排序树创造出来就是为了使我们在查找中更加方便(二分查找),由于二叉排序树的特殊性,使得其左边的值总是小于根节点,右边的值总是大于根节点,可以发现当我们用中序遍历这棵树时,它的结果是一个有序的序列:1 3 4 6 7 8 10 13 14;

它的节点的结构体和一般二叉树一样;

typedef struct TreeNode
{
	int data;
	struct TreeNode* left, * right;
}Node;

而构建一棵二叉排序树也很简单,按照它的性质插入即可;

//默认没有重复的key的情况
void insert(Node** root, int key)
{
	Node* prev = NULL;//用来指向正确的插入位置的父节点
	Node* temp = *root;
	while (temp != NULL)
	{
		//追随root的上一个位置
		prev = temp;
		//如果插入的值小于该节点 往左边找
		if (key < temp->data)
		{
			temp = temp->left;
		}
		//如果插入的值大于该节点 往右边找
		else if (key > temp->data)
		{
			temp = temp->right;
		}
	}
	//找到了要插入的位置
	Node* newnode = (Node*)malloc(sizeof(Node));
	if (newnode == NULL)
	{
		printf("ERR\n");
		return;
	}
	else
	{
		newnode->data = key;
		newnode->left = NULL;
		newnode->right = NULL;
		if (*root == NULL)//如果根节点没有初始化
		{
			*root = newnode;
		}
		else if (key < prev->data)
		{
			prev->left = newnode;
		}
		else if (key > prev->data)
		{
			prev->right = newnode;
		}
	}
}

二叉排序树的重点在于删除操作。对于二叉排序树的删除,我们一般分为三种情况:

  • 第一种情况:删除的是叶子节点,这时候直接删除就行了,因为删除它不会对整棵树产生影响;
  • 第二种情况:删除的节点有一个孩子(无论左右),这时候需要把它的孩子移动到被删除节点的位置就行了;
  • 第三种情况:删除的节点有两个孩子,这时候需要找到该节点的直接前驱或者直接后继来替换删除节点;

前两种情况都好理解,我们详细说说第三种情况。以上文中的有序序列1 3 4 6 7 8 10 13 14为例,假如我们要删除值为8的节点,要想不对整个有序序列产生影响,删除后的结果应该是1 3 4 6 7 10 13 14,也就是我们要找到值是7或者值是10的节点替换8节点,然后按照情况一、二删除原来的7或者10,。这个7节点就是删除节点的直接前驱,同理节点10是删除节点的直接后继;

要找到这个直接前驱或者直接后继也很简单:从下图中我们可以看出,直接前驱节点一定是删除节点的左子树中的最大值,直接后继节点一定是删除节点的右子树中的最小值

以直接前驱节点7为例,如果待删除的节点左子树存在,我们只需要先访问这个左子树,然后一直访问它的右子树即可。

/*真正的删除操作*/
void del(Node* node, Node* prev)
{
	Node* temp = NULL;
	//只有左子树或者只有右子树的情况 把要删除的节点删除 并把孩子替换上去
	if (node->left == NULL && node->right != NULL)
	{
		temp = node;
		temp = temp->right;
		node->data = temp->data;
		node->left = temp->left;
		node->right = temp->right;
		free(temp);
	}
	else if (node->right == NULL && node->left != NULL)
	{
		temp = node;
		temp = temp->left;
		node->data = temp->data;
		node->left = temp->left;
		node->right = temp->right;
		free(temp);
	}
	//叶子节点的情况
	else if (node->right == NULL && node->left == NULL)
	{
		//左叶子的情况
		if (node->data < prev->data)
		{
			prev->left = NULL;
		}
		else
		{
			prev->right = NULL;
		}
		free(node);
	}
	//左右子树都不为空的情况
	else
	{
		temp = node;
		Node* s = node;//指向左子树的最大值的指针
		//找左子树的最大值
		s = s->left;
		while (s->right != NULL)
		{
			temp = s;//这里的temp指向s的父节点
			s = s->right;
		}
		//替换数据
		node->data = s->data;
		//还要删除s节点 又分两种情况
		//但是不管哪种情况此时的s要么没有孩子要么只有左孩子
		if (temp!= node)
		{
			//说明s往下走了很多步 这个时候s一定在temp的右边
			temp->right = s->left;
		}
		else
		{
			//这个时候s一定在temp的左边
			temp->left = s->left;
		}
	}
}

/*封装删除操作*/
void deleteNode(Node* root, int key)
{
	if(root == NULL)
	{
		return;
	}
	else
	{
		static Node* prev = NULL;//prev是删除节点的父节点
		//递归地去寻找要删除的点
		if (key < root->data)
		{
			prev = root;
			deleteNode(root->left, key);
		}
		else if (key > root->data)
		{
			prev = root;
			deleteNode(root->right, key);
		}
		else
		{
			//删除
			del(root, prev);
		}
	}
}

用中序遍历测试结果 

后话

正常来说,一棵二叉排序树的复杂度取决于树的高度h,但如果我插入的值是12345...呢?它是不是就成了我们所说的复杂度为O(N)的最坏的情况——斜树,为保证查找效率,人类历史上的第一棵自平衡树——二叉平衡树就此诞生!我们下次将详细讲解二叉平衡树的增删查改。

猜你喜欢

转载自blog.csdn.net/ZER00000001/article/details/125948512