数据结构之:二叉树

今天复习到了二叉树,首先是了解一下二叉树的一下定义:

二叉树是每个节点最多有两个子树的树结构,通常称为左/右子树。如图所示:

其中深度Depth可以理解为层数,那么第i层则有2^(i-1)那么多的节点。其中没有左右子树的节点称为叶节点,而其他的称为根节点。例如值为8~15的节点就是叶子节点。

而树的类型也有很多种,其中基本的有:

1.完全二叉树——假如一棵树的最大深度为D,那么1到D-1层的节点都是排满的,第D层的节点都是从左到右饱满的。例如上图,假设去掉12~15,就可以看到,1-3层是饱满的,而第4层是从左到右排的。

2.满二叉树——一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。如上图则是一个满二叉树

3.平衡二叉树(又称AVL树)——它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。例如下图:

其中还有一些关于树的术语:

-根结点:位于最低深度的结点,没有入度。例如上图的8。

-叶子结点:

-树的结点:也就是包含了一个数据元素和若干个指向子树的结点。如上图的根结点,就包含了数据8和指向3,10结点的指针

-孩子结点:结点指向的结点就是孩子结点,例如3和10就是8的孩子结点,则8是3和10的父亲结点

-兄弟节点:同一个父亲指向的结点,例如3和10

等等....

以下是二叉树的抽象数据结构ADT(只实现了部分代码):

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

using namespace std;

template <class T>
class BinaryTreeNode {
	friend class BinaryTree<T>; //定义为友元类,则二叉树就可以操作节点的私有成员 
	private :
		T info;
		BinaryTreeNode<T> *left;
		BinaryTreeNode<T> *right;
	public :
		BinaryTreeNode();
		BinaryTreeNode(const T& ele);
		BinaryTreeNode(const T& ele, BinaryTreeNode<T> *l, BinaryTreeNode<T> *r);
		T value() const; //返回值 
		BinaryTreeNode<T> *leftchild() const; //返回左子树
		BinaryTreeNode<T> *rightchild() const; //返回右子树
		void setLeftchild(BinaryTreeNode<T> *); //设置左子树
		void setRightchild(BinaryTreeNode<T> *); //设置右子树
		void setValue(const T& val);             //设置数据
		bool isLeaf() const;                   //判断是否是叶节点
		BinaryTreeNode<T>& operator= (const BinaryTreeNode<T> &Node); //重载=	
};

template <class T>
class BinaryTree {
	private :
		BinaryTreeNode<T> *root;
	public :
		BinaryTree() {
			root = NULL;
		}
		~BinaryTree() {
			DeleteBinaryTree(root);
		}
		bool isEmpty() const {
			return (root == NULL);
		}
		BinaryTreeNode<T> *Root() {
			return root;
		}
		BinaryTreeNode<T> *Parent(BinaryTreeNode<T> *current); //返回父节点
		BinaryTreeNode<T> *LeftSibling(BinaryTreeNode<T> *current); //返回左节点
		BinaryTreeNode<T> *RightSibling(BinaryTreeNode<T> *current);
		void CreateTree(const T& info, BinaryTree<T>& leftTree, BinaryTree<T>& rightTree);
		void PreOrder(BinaryTreeNode<T> *root); //前序遍历
		void InOrder(BinaryTreeNode<T> *root); // 中序遍历
		void PostOrder(BinaryTreeNode<T> *root); //后序遍历
		void LevelOrder(BinaryTreeNode<T> *root); //按层次遍历
		void DeleteBinaryTree(BinaryTreeNode<T> *root); //删除二叉树或其子树 
};

//前序非递归版本 
template <class T>
void BinaryTree::PreOrder(BinaryTreeNode<T> *root) {
	stack<BinaryTreeNode<T> *> Stack;
	BinaryTreeNode<T> *pointer = root;
	Stack.push(NULL);
	while (pointer) {
		Visit(pointer -> value());
		if (pointer -> rightchild() != NULL) {
			Stack.push(pointer -> rightchild());
		}
		if (pointer -> leftchild() != NULL) {
			pointer = pointer -> leftchild();
		} else {
			pointer = Stack.top();
			Stack.pop();
		}
	}
}
//递归版本
template <class T>
void BinaryTree::PreOrder(BinaryTreeNode<T> *root) {
	if (root != NULL) {
		Visit(root -> value()); //前序 
		PreOrder(root -> leftchild());
		//Visit(root -> value());  //中序 
		PreOrder(root -> rightchild());
		//Visit(root -> value());  //后序 
	}
}
//中序非递归版本
template <class T>
void BinaryTree::InOrder(BinaryTreeNode<T> *root) {
	stack<BinaryTreeNode<T> *> Stack;
	BinaryTreeNode<T> *pointer = root;
	Stack.push(NULL);
	while (pointer || !Stack.empty()) {
		if (pointer) {
			Stack.push(pointer);
			pointer = pointer -> leftchild();
		} else {
			pointer = Stack.top();
			Stack.pop();
			Visit(pointer -> value());
			pointer = pointer -> rightchild();
		}
	}
}

//宽度优先搜索(层次遍历二叉树)
template <class T>
void BinaryTree::LevelOrder(BinaryTreeNode<T> *root) {
	queue<BinaryTreeNode *> Queue;
	BinaryTreeNode<T> *pointer = root;
	if (root) {
		Queue.push(root);
	}
	while (!Queue.empty()) {
		pointer = Queue.front();
		Queue.pop();
		Visit(pointer -> value());
		if (pointer -> leftchild()) {
			Queue.push(pointer -> leftchild());
		}
		if (pointer -> rightchild()) {
			Queue.push(pointer -> rightchild());
		}
	}
} 


  而有一些二叉树是有着比较特殊的结构的,例如二叉搜索树,其中每个结点的值都大于左子树的结点的值,而 小于右子树结点的值。如下图:

这样结构的树在搜索上就会比一般的树要快,因为平均只需要搜索logn个结点就可以完成,时间复杂度是O(logn),而二叉搜索树的中序遍历就是整个树元素的升序序列。而二叉搜索树最重要的操作是插入结点和删除结点,下面只贴上这两个代码,目的是理解一下插入删除的一个过程:

//二叉搜索树的添加结点
template <class T>
void BinarySearchTree::appendNode(BinarySearchTree<T> *root, const T value) {
	if (root == NULL)  {
		root = new BinaryTreeNode(value);
	} else {
		BinarySearchTree<T> *temp = root;
		//这里考虑不重复元素的情况 
		if (value < temp -> value() && temp -> leftchild() == NULL) {
			BinaryTreeNode<T> newNode = new BinaryTreeNode<T>(value);
			temp -> setLeftchild(new Node);
		} else if (value > temp -> value() && temp -> rightchild() == NULL) {
			BinaryTreeNode<T> newNode = new BinaryTreeNode<T>(value);
			temp -> setRightchild(new Node);
		} else {
			(value < temp) ? appendNode(temp -> leftchild(), value) : appendNode(temp -> rightchild(), value);
		}
	}
}

//二叉搜索树的删除结点 
template <class T>
void BinarySearchTree::removeNode(BinarySearchTree<T> *root, const T value) {
	//首先是找到该值的结点 
	if (root == NULL) {
		cout << "false" << endl;
	} else if (value < root -> value()) {
		removeNode(root -> leftchild(), value);
	} else if (value > root -> value()) {
		removeNode(root -> rightchild(), value);
	} else {
		BinaryTreeNode<T> *temp = root;
		if (rt -> leftchild() == NULL) {
			root = root -> rightchild();
		} else if (rt -> rightchild() == NULL) {
			root = root -> leftchild();
		} else {
			temp = deletemin(root -> rightchild());
			root -> setValue(temp -> value());
		}
		delete temp;
	}
}
//删除树中的最小最小结点 
template <class T>
BinaryTreeNode<T> *BinarySearchTree::deletemin(BinarySearchTree<T> *root) {
	if (root -> leftchild() != NULL) {
		return deletemin(root -> leftchild());
	} else {
		BinarySearchTree<T> *temp = root;
		root = root -> rightchild();
		return temp;
	}
}


而完全二叉树还有一个非常常用的地方,那就是堆。堆常用有两种,一种是最小堆,一种是最大堆。其中以最小堆为例:

 最小堆是一个关键码序列 {A0, A1, ....An-1},其中有着

Ai ≤ A(2i+1) 和 Ai 小于等于 A(2i+2) (其中i=0, 1, ...n/2-1向下取整)。那么类似也可以定义最大堆。

 

如图所示,父节点是肯定小于子节点,但是两个兄弟节点之间的序列是无关的。因为完全二叉树的性质,所以我们可以用着数组来存储。而对应的节点之间的关系可以用i,2i+1,2i+2来决定。说起一个最小堆的数据结构,那么它肯定得有自己的一套方法,才能做到构建、增删查等的操作,以下我们给出最小堆的ADT,以及重要函数部分的实现:

#include <iostream>

using namespace std;

template <class T>
class MinHeap {
	private :
		T* heapArray;
		int CurrentSize;   //堆中元素的数目
		int MaxSize;       //堆所能容纳最大元素数目
		void BuildHeap();  //建堆
	public :
		//构造函数和析构函数 
		MinHeap(const int size);
		~MinHeap();
		//操作函数
		bool isLeaf(int pos) const;     //假如是叶子结点则返回true
		int leftchild(int pos) const;   //返回左孩子的位置,先判断是否存在,再返回
		int rightchild(int pos) const;
		int parent(int pos) const;      //返回父节点位置
		bool Remove(int pos, T& node);  //删除给定下标的元素
		bool Insert(const T& newNode);  //向堆中插入新元素
		T& RemoveMin();                 //从堆顶删除最小值
		void SiftUp(int pos);           //从pos位置向上开始调整,使序列最小堆化
		void SiftDown(int pos);
};

template <class T>
void MinHeap<T>::SiftDown(int pos) {
	int i = pos;            //父节点 
	int j = 2 * i + 1;      //子节点
	T temp = heapArray[i];
	while (j < CurrentSize) {
		if (j < CurrentSize - 1 && heapArray[j] > heapArray[j + 1]) {
			j++;     //j+1位置的子节点元素值更小 
		}
		if (temp > heapArray[j]) {
			i = j;
			j = 2 * i + 1;
		} else {
			break;
		}
	}
	heapArray[i] = temp;
}

template <class T>
void MinHeap<T>::SiftUp(int pos) {
	int temp_pos = pos;
	T temp = heapArray[pos];
	while ((temp_pos > 0) && (heapArray[parent(temp_pos)] > temp)) {
		heapArray[temp_pos] = heapArray[parent(temp_pos)];
		temp_pos = parent(temp_pos);
	}
	heapArray[temp_pos] = temp;
}

//最小堆的建立 
void MinHeap<T>::BuildHeap() {
	for (int i = CurrentSize / 2 - 1; i >= 0; i--) {
		SiftDown(i);
	}
}

//最小堆插入元素
template <class T>
bool MinHeap<T>::Insert(const T& newNode) {
	if (CurrentSize == MaxSize) {
		return false;
	}
	heapArray[CurrentSize] = newNode;
	SiftUp(CurrentSize);
	CurrentSize++;
	return true;
}

//最小堆删除元素
 template <class T>
bool MinHeap<T>::Remove(int pos, T& node) {
	if ((pos < 0) || (pos >= CurrentSize)) {
		return false;
	}
	T temp = heapArray[pos];
	heapArray[pos] = heapArray[--CurrentSize];
	if (heapArray[parent(pos)] > heapArray[pos]) {
		SiftUp(pos);
	} else {
		SiftDown(pos);
	}
	node = temp;
	return true;
}

而最小堆的应用也比较广,例如优先队列,用于处理值最小的优先,诸如此类。

还有一个是huffman树,其中huffman树就可以用最小堆来实现。首先huffman树是一个用来解决编码中字符出现的频率不一样所带来的过多消耗内存的问题。

例如,一个编码中,一共出现了10次的A,5次的B,和1次的C。这样的话,我们要用二进制来代表这些数的话,可能就要用到2位的二进制数,其中A表示00,B表示01,C表示10。这样的话一共用了(10+5+1)*2=32bits。但是能不能把常出现的一些字符设置的二进制位更短,例如0表示A,10表示B,11表示C,这样的话一共就用了21bits,这样就大大减少了内存的使用。所以我们称这种压缩为huffman编码。

其中huffman编码需要满足两个条件:

1.不能出现一个字符代表的二进制,是另一个字符的二进制的前缀。例如假如1代表A,00代表B,001代表C,那么这样001就可能有不同的编码了。

2.最常用的字符一定是位于深度更小的结点上。


而要解决上面的第一个问题,那就要做到只有叶结点代表一个字符。并且左结点表示0,右结点表示1.如图:


结点的值表示出现的频率。所以我们要构造的就是这样的一棵树。c++代码如下(部分实现):

#include <iostream>

using namespace std;

template <class T>
class HuffmanTree {
	private :
		HuffmanTreeNode<T> *root;
		void MergeTree(HuffmanTreeNode<T> &ht1, HuffmanTreeNode<T> &ht2, HuffmanTreeNode<T> &parent);
	public :
		HuffmanTree(T weight[], int n);
		virtual ~HuffmanTree() {
			DeleteTree(root);
		}
};

template<class T>
HuffmanTree<T>::HuffmanTree(T weight[], int n) {
	MinHeap< HuffmanTreeNode<T> > heap;
	HuffmanTreeNode<T> *parent, &leftchild, &rightchild;
	HuffmanTreeNode<T> *NodeList = new HuffmanTreeNode<T>[n];
	for (int i = 0; i < n; i++) {
		NodeList[i].element = weight[i];
		NodeList[i].parent = NodeList[i].left = NodeList[i].right = NULL;
		heap.Insert(NodeList[i]);
	}
	
	for (int i = 0; i < n - 1; i++) {
		parent = new HuffmanTreeNode<T>;
		leftchild = heap.RemoveMin();
		rightchild = heap.RemoveMin();
		MergeTree(leftchild, rightchild, parent);
		heap.Insert(*parent);
		root = parent;
	}
	delete []NodeList;
}

template<class T>
void HuffmanTree::MergeTree(HuffmanTreeNode<T> &ht1, HuffmanTreeNode<T> &ht2, HuffmanTreeNode<T> &parent) {
	parent.leftchild = ht1;
	parent.rightchild = ht2;
	parent.weight = ht1.element + ht2.element;
}




  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值