一:线索二叉树
二叉树遍历实际是将复杂的非线性结构转换为线性结构,一个n个节点的二叉链表,一共2n个指针域,n+1个分支,也就是说只有n+1个指针域是有值的,另外n-1个是空的;
线索二叉树指的就是,利用这些空的指针域,存储上这个节点的前驱或者后继节点,前驱和后继依据遍历得到.
相当于把遍历的信息附加在链表上.这样的二叉链表叫做线索链表.
浪费的指针域
如下图,中序遍历这个二叉树,把空着的指针指向前驱或者后继节点
填充后继
填充前驱
但是现在又有一个问题,我们不知道一个节点,它的指针域存的到底是孩子还是前驱/后继,因此需要作出区分.
增加ltag和rtag做标记,ltag为0表示左孩子,为1表示前驱,rtag表示右孩子或者后继.
增加两个tag
增加两个ta
swift实现:
class Node{
var name : String
weak var leftChild : Node?
weak var rightChild : Node?
var ltag = 0
var rtag = 0
init(_ dataName:String) {
name = dataName
}
deinit {
print("node:\(name) release")
}
}
线索链表已经是一个双向链表,在swift中指针不能再直接引用了,否则无法释放,因此需要weak引用;
ltag和rtag初始化为0比较方便;
var nodes = [Node]()
var pre : Node?
func createBinaryTree(_ list:[String]) -> Node?{
if(list.count == 0){
return nil
}
var index = 0
func createTree() -> Node?{
let str = list[index]
var tree : Node?
index += 1;
if index < list.count && str != "#"{
tree = Node.init(str)
nodes.append(tree!)
tree!.leftChild = createTree()
tree!.rightChild = createTree()
}
return tree
}
return createTree()
}
设置一个全局的指针pre,用于标记当前处理的那个节点;
初始化一个数组nodes用于真正的节点存储;
根据前序遍历生成二叉树,每个节点存储在nodes中.
func binaryTreeThread(_ root : Node?){
guard let node = root else {
return
}
if node.leftChild == nil{
node.ltag = 1
node.leftChild = pre
}
if pre?.rightChild == nil{
pre?.rtag = 1
pre?.rightChild = node
}
pre = node
if node.ltag == 0{
binaryTreeThread(node.leftChild)
}
if node.rtag == 0{
binaryTreeThread(node.rightChild)
}
}
实现线索化,如果leftChild是空的,则把leftChild设置为前一个节点pre,也就是前驱;如果前一个节点的rightChild是空的,就把rightChild设置为当前的节点,也就是后继,然后更新pre;最后,为了防止循环无法停止,需要在tag为0的情况下进行递归.
func binaryTreeForEach(_ root : Node?){
guard let node = root else {
return
}
print(node.name + "前驱是\(node.ltag == 1 ? node.leftChild!.name : "")" + "后继是\(node.rtag == 1 ? node.rightChild!.name : "")")
if node.ltag == 0{
binaryTreeForEach(node.leftChild)
}
if node.rtag == 0{
binaryTreeForEach(node.rightChild)
}
}
现在遍历方法也要修改,不然会无限循环;
在遍历的时候输出前驱和后继
let node = createBinaryTree(["A","B","#","D","#","#","C","#","#"])
binaryTreeThread(node)
binaryTreeForEach(node)
nodes.removeAll()
pre = nil
最后调用方法,然后置空,全部释放.
输出结果
现在我们添加一个头结点,让头结点的lchild指向中序遍历的第一个节点N1,rchild指向中序遍历的最后一个节点N2;
并且N1的lchild指向头结点,也就是头结点是N1的前驱,N2的rchild指向头结点,也就是头结点是N2的后继.
这样一来,从N1开始,顺序访问后继一直到头结点,就可以遍历整个二叉树,反过来从N2开始顺序访问前驱也可以.
二:赫夫曼树
首先看一个例子,学生的考试成绩需要按照分段筛选到不同的评优等级;
假如学生的成绩际分布是这样的:
成绩分布
那么下面这段代码虽然合理但是显然其效率是有优化空间的.
条件筛选
树形结构
我们调整一下,把分布概率更大的节点放在前面,显然效率会提高.
调整后的树
1.定义
有这样两个二叉树(图1-1),我们给叶节点加上权重,权重根据业务有具体的意义;
从一个节点到另一个节点叫做路径,每间隔一个节点,路径长度增加1;
从根节点到每一个节点的路径长度之和, 叫做树的路径长度;下图中左边的树,根节点到其他节点一共8条路径,路径长度是1+1+2+2+3+3+4+4 = 20; 右边是1+2+3+3+2+1+2+2 = 16.
节点的路径长度乘上该节点的权,就叫带权路径长度;
而树的带权路径长度是所有的叶节点的带权路径长度之和;左边是1x5+2x15+3x40+4x30+4x10 = 315;右边是3x5+3x15+2x40+2x30+2x10 = 220.
带权路径长度(WPL)最短的二叉树树,就叫做赫夫曼树.
2.构造赫夫曼树
首先将权值最小的两个节点组合成一个子树N1,注意权值小的设置为左孩子,现在N1的权值是15;
然后将N1和权值倒数第三小的节点组成子树N2,同样权值小的设置为左,也就是15(N1)和15(B)比较,这里左右都行,现在N2的权值是30;
重复上面的过程,当放置到D时,N4的权值是60,大于C的40,此时C是左孩子,然后只剩一个根节点,设置为T;
赫夫曼树完成
3.赫夫曼编码
现在要发送一段文字"BADCADFEED",一共出现六个字母ABCDEF,进行编码
假设六个字母的权值是A 27,B 8,C 15, D 15, E 30, F 5;如下图左边的树.
接着把这个树的左孩子改成权值0,右孩子改成权值1;如下图右边的树.
树和改造后的树
现在每个字母根据路径就得到了唯一的编码;
新的编码表
结果对比
并且它还要一个好处,就是不会出现包含其他编码字串的情况出现;
解析的时候也通过赫夫曼树来解析,只要一个个数字顺着路径走就可以了.
解析
定义:设需要编码的字符集为{d1,d2...dn},各个字符出现的频率是{w1,w2....wn},以d1,d2...dn为叶节点,以w1,w2...wn作为节点的权值来构造哈赫夫曼树,规定该树的左分支为0,右分支为1,则字符的路径对应的编码就是赫夫曼编码.
网友评论