二叉树基础运用——学习笔记

回想刚学二叉树的时候,被二叉树删除搞的要死要活的,后来学习了AVL树、红黑树才慢慢熟练了二叉树的基础操作。篇幅会有点长~慢慢欣赏~

我就当各位都是明白什么是树,什么叫二叉树。在这里我就不多解释了~如果这些都不清楚的话,可能需要先补充些相关知识再看会比较好(有空我会写一篇介绍树的文章~)

一.创建结构体

struct Node {
	int key;
	int count;
	Node* parent;
	Node* left;
	Node* right;
};

二叉树结构的基本构成,数据key,父节点parent,左子节点left,右子节点right。

我擅自加了个count,由于我们做数据排序的时候,有时候会遇到相同的key值,那么这个时候传统的操作是当成重复数据直接抛掉,我个人觉得抛掉有点可惜哈,那么就加个count来记录插入相同数据的个数。

顺便一提,初学的时候会觉得parent好像没什么用,实际上不用parent也是可以实现二叉树的,但是有了parent一些操作会变得更加简单,例如删除操作。

当然,上面的count和parent不是必须的。具体结构根据实际需要进行调整即可。

二.建立根节点

Node* root;

void InitRoot() {
	root = (Node*)malloc(sizeof(Node));
	root->key = 0;
	root->count = 0;
	root->parent = NULL;
	root->left = NULL;
	root->right = NULL;
}

这里就不多说,很简单。

三.插入数据

首先二叉树是有个规则的,就是对于树中的每个子树的根节点,左边的节点都要小于根节点,右边节点都要大于根节点。也就是有下面这个结构:

       中
      /  \
    小    大

举个例子,我们插入[7,3,5,1,9,8,2,4]时,按照上面规则来排,最终得到这么一棵树:

                7
             /      \
            3         9
         /    \      /
        1      5    8
         \    /
          2  4

有兴趣可以自己排一排练习下看看我们是不是排成一样的。

根据这个规则我们来写插入函数:

void InsertTree(Node *root, int key) {
	//首先判断根节点是否有存数据
	if (root->count == 0) {
		root->key = key;
		root->count++;
	}
	else {
		Node* node = root;//创建子节点
		Node* parent = root->parent;//子节点的父节点

		//找到子节点的父节点
		while (node != NULL) {
			parent = node;

			if (key == node->key) {//假如该节点在树中已有,count+1返回
				node->count++;
				return;
			}
			else if(key < node->key){//小于时往左节点寻找
				node = node->left;
			}
			else if (key > node->key) {//大于时向右节点寻找
				node = node->right;
			}
		}

		//给子节点赋值
		node = (Node*)malloc(sizeof(Node));
		node->key = key;
		node->count = 1;
		node->parent = parent;
		node->left = NULL;
		node->right = NULL;

		//把子节点挂到父节点上
		if (key < parent->key) {
			parent->left = node;
		}
		else {
			parent->right = node;
		}
	}
}

四.查找数据

查找的情况和插入很相似,同样运用了左小右大的规则,看代码!

Node* Find(Node* root, int key) {
	if (root->count != 0) {//判断根节点是否有数据
		Node* node = root;

		while (node!=NULL)
		{
			if (key == node->key) {//找到节点,直接返回
				return node;
			}
			else if(key < node->key) {//小于时,找向左节点
				node = node->left;
			}
			else if(key > node->key){//大于时,找向右节点
				node = node->right;
			}
		}
	}
	return NULL;
}

相信学会了插入,查找就难不倒你了。

五.删除数据

重头戏来了!删除是应该是最难的部分了,为什么难?因为删除的时候有三种情况:

1.被删除的是叶子节点,左右子节点都为空

2.被删除的节点,左右子节点有一个为空

3.被删除的节点,左右子节点都不为空

我们一个一个来分别处理。

首先,被删除的是叶子节点,我们只需要让父亲节点连接的左或右节点为空即可

         父              父
        /               /
       删       -->   NULL
     /   \
   NULL  NULL

代码如下:

if (node->left == NULL&&node->right == NULL) {
	Node* parent = node->parent;//获取父节点

	if (parent == NULL) {//判断是不是根节点
		root->key = 0;
		root->count = 0;
	}
	else {
		if (node == parent->left) {
			parent->left = NULL;
		}
		else {
			parent->right = NULL;
		}
	        free(node);//释放
	}
}

当被删除的节点只有一个子节点时,我们只需要把子节点替代删除节点即可:

            父                 父
          /                   /
         删        -->      子
       /   \
     NULL   子

代码如下:

if((node->left!=NULL&&node->right==NULL)||(node->left==NULL&&node->right!=NULL)){
	Node* parent = node->parent;
	Node* child = node->left == NULL ? node->right : node->left;
	child->parent = parent;

	if (parent == NULL) {//判断是不是根节点
		root = child;
	}
	else {
		if (node == parent->left) {
			parent->left = child;
		}
		else {
			parent->right = child;
		}
	        free(node);
	}
}

当被删除节点的两个子节点都不为空时,这时候我们需要找一个节点来替代删除节点。同时我们又需要使得节点左小右大的性质不被破坏,所以我们可以以删除结点为根节点,可以找其左子树中的最大节点或者右子树中的最小节点来替代:

           父                      父                               父
          /                       /                                /
         删         -->        左MAX              或者           右MIN
       /   \                   /   \                             /   \
  左子树   右子树       左子树-Max    右子树                  左子树    右子树-MIN

上代码:

if (node->left != NULL&&node->right != NULL) {
	Node* right_min=node->right;
        
	while (right_min->left == NULL) {//找到右子树中的最小值
		right_min = right_min->left;
	}

	int num= right_min->key;
	node->count = right_min->count;
	DeletTree(num);                //先删除右子树最小节点
	node->key = num;              //将删除节点的值进行替换
}

我选择找右子树的最小节点。

可能有人会疑问这里的DeleteTree(num),因为我们要先删除找到的右子树最小节点,再给当前节点赋值,假如先赋值再删除,就只会把当前节点删除(因为此时的key已经改变了),这样不仅没达到效果,还会引起错误。

上删除的完整代码:

void DeletTree(int key) {
	Node* node = Find(root, key); //找到需要删除的节点

	if (node == NULL) {         //判断该节点是不是存在
		return;
	}

        Node* parent=node->parent;
	if (node->left != NULL&&node->right != NULL) {  //子节点都存在时
		Node* right_min = node->right;

		while (right_min->left != NULL) {           //找到其右子树最小节点
			right_min = right_min->right; 
		}

		int num = right_min->key;
		node->count = right_min->count;
		 
		DeletTree(num);                            //删除右子树最小节点

		node->key = num;                           //将右子树最小值赋给该节点
	}
	else if(node->left==NULL&&node->right==NULL){  //当子节点都为空时
		if (parent == NULL) {                      //先判断是否为根节点
			root->key = 0;
			root->count = 0;
		}
		else {
			if (node == parent->left) {
				parent->left = NULL;
			}
			else {
				parent->right = NULL;
			}
			free(node);
		}
		
	}
	else {                                    //剩下情况是一个节点为空
		Node* child = node->left == NULL ? node->right : node->left;
		child->parent = parent;

		if (parent == NULL) {
			root = child;
		}
		else {
			if (node == parent->left) {
				parent->left = child;
			}
			else {
				parent->right = child;
			}
			free(node);
		}
	}
}

PS:由于考虑到有的同学可能递归学的比较好,再提供一份递归版的删除

Node* DeletTree2(int key, Node* node) {
	if (node != NULL) {
		if (key == node->key) {
			if (node->left != NULL&&node->right != NULL) {
				Node* right_min = node->right;

				while (right_min->left != NULL) {
					right_min = right_min->left;
				}

				node->key = right_min->key;
				node->count = right_min->key;
				right_min->parent = DeletTree2(right_min->key, right_min->parent); //注意这里,巧妙运用了parent
			}
			else {
				if (node->left == NULL&&node->right == NULL) {
					free(node);
					node = NULL;
				}
				else {
					Node* child = node->left == NULL ? node->right : node->left;
					free(node);
					node = child;
				}
			}
		}
		else if (key < node->key) {
			node->left = DeletTree2(key, node->left);
		}
		else {
			node->right = DeletTree2(key, node->right);
		}
	}

	return node;
}

使用的时候注意为 root=(DeletTree2(key,root)),防止删除的点刚好是root,此时root会改变。

大致思路和前面的是一样的,不多加注释了,有兴趣可以研究研究。

六.数据遍历

到了这里就很轻松了。遍历的情况分为前序遍历,中序遍历,后序遍历。具体是看我们访问节点和其两个子节点的顺序来决定。代码很简单,但是由于运用到了递归,可能在理解上会多少有些抽象,我尽量解释清楚吧。先上代码:

// N=node , L=left , R=right;
//NLR 前序遍历
//LNR 中序遍历 
//LRN 后序遍历

void NLR(Node* node) {                      //前序遍历
	if (node == NULL) {
		return;
	}

	for (int i = 0; i < node->count; i++) {
		printf("%d", node->key);
	}

	NLR(node->left);
	NLR(node->right);
}

void LNR(Node* node) {                    //中序遍历
	if (node == NULL) {
		return;
	}

	LNR(node->left);

	for (int i = 0; i < node->count; i++) {
		printf("%d", node->key);
	}

	LNR(node->right);
}

void LRN(Node* node) {                //后序遍历
	if (node == NULL) {
		return;
	}

	LRN(node->left);
	LRN(node->right);

	for (int i = 0; i < node->count; i++) {
		printf("%d", node->key);
	}
}

可以看出来,其实就是中间一段代码的执行顺序不同:

for (int i = 0; i < node->count; i++) {
	printf("%d", node->key);
}

以第三节的[7,3,5,1,9,8,2,4]为例:

                7
             /      \
            3         9
         /    \      /
        1      5    8
         \    /
          2  4
输入:[7,3,5,1,9,8,2,4]
前序:[7,3,1,2,5,4,9,8]
中序:[1,2,3,4,5,7,8,9]
后序:[9,8,7,5,4,3,2,1]

为什么仅仅调整了顺序就会有很大的不同呢?这就是递归的妙处所在,以中序输出为例:

void LNR(Node* node) {
	if (node == NULL) {
		return;
	}

	LNR(node->left) {
		if (node == NULL) {
			return;
		}

		LNR(node->left) {
			......//重复
		};

		for (int i = 0; i < node->count; i++) {
			printf("%d", node->key);
		}

		LNR(node->right);
	};

	for (int i = 0; i < node->count; i++) {
		printf("%d", node->key);
	}

	LNR(node->right);
}

实际上在函数 LNR()递归调用自身的时候,我们就进入另一个函数了,只不过这个函数的操作步骤和 LNR()自己本身一样,这样不断递归下去,那么直到 某一次递归的node=NULL时才开始执行其他操作,所以最先访问的是二叉树最左端的数据。

不知道这样解释能不能理解,不行就自己多敲点代码吧~~~熟能生巧

七、总结

好了终于又到说拜拜的时候了,这里主要实现了二叉树的增删查和遍历,没有改(对于二叉树要写改的话就是先删再增)。写的不太好的地方多包涵,,,大家都是新手,互相学习吧。

猜你喜欢

转载自blog.csdn.net/jjwwwww/article/details/81051137