二叉樹的實(shí)現(xiàn)原理
- 組成結(jié)構(gòu)
二叉樹是由根節(jié)點(diǎn)與左右子樹連接起來的數(shù)據(jù)結(jié)構(gòu)栈顷。所以實(shí)現(xiàn)一棵二叉樹昧甘,有兩個必要結(jié)構(gòu)數(shù)據(jù):
-
節(jié)點(diǎn)數(shù)據(jù)結(jié)構(gòu)
用來連通各個節(jié)點(diǎn) -
樹結(jié)構(gòu)
用來包裹節(jié)點(diǎn)數(shù)據(jù)
- 節(jié)點(diǎn)數(shù)據(jù)的實(shí)現(xiàn)
節(jié)點(diǎn)數(shù)據(jù)有兩點(diǎn)主要功能:
- 存放數(shù)據(jù)元素
- 能夠連接左右節(jié)點(diǎn)
依據(jù)這些特點(diǎn)会钝,可以使用下面的方式構(gòu)建節(jié)點(diǎn)數(shù)據(jù):
typealias BinaryTreeCompare = Comparable & Equatable
class Child<E: BinaryTreeCompare> : Equatable{
static func == (lhs: Child<E>, rhs: Child<E>) -> Bool {
let compareResult =
lhs.element == rhs.element &&
lhs.left == rhs.left &&
lhs.right == rhs.right
return compareResult
}
var element: E
var left: Child?
var right: Child?
init(element: E, left: Child?, right: Child?) {
self.element = element
self.left = left
self.right = right
}
func isLeaf() -> Bool { left == nil && right == nil }
func hasTwoChild() -> Bool { left != nil && right != nil }
}
- 二叉樹結(jié)構(gòu)內(nèi)容的實(shí)現(xiàn)
樹的結(jié)構(gòu)是對其節(jié)點(diǎn)的包裝,有這以下幾個特點(diǎn):
- 每棵二叉樹都有一個
根節(jié)點(diǎn)root
- 每一棵二叉樹都有著其自己的容量大小
size
- 添加新的
節(jié)點(diǎn)
- 移除每一棵樹的某個節(jié)點(diǎn)
節(jié)點(diǎn)
- 清除一棵樹的
所有節(jié)點(diǎn)
- 查找某個特定節(jié)點(diǎn)
是否存在
依據(jù)其功能特點(diǎn),就抽象出了幾個必要的基礎(chǔ)接口,如下:
protocol BinaryTreeProtocol : CustomStringConvertible {
associatedtype T: BinaryTreeCompare
//根節(jié)點(diǎn)
var root: Child<T>? { set get }
//節(jié)點(diǎn)添加
mutating func add(element: T)
//清除二叉樹所有節(jié)點(diǎn)
mutating func clear()
//移出特定節(jié)點(diǎn)元素
mutating func remove(element: T)
//獲取容量
func size() -> Int
//是否為空
func isEmpty() -> Bool
//是否包含某個特定元素節(jié)點(diǎn)
func contains(element: T) -> Bool
}
具體實(shí)現(xiàn)
結(jié)構(gòu)構(gòu)建完成之后分飞,依據(jù)抽象內(nèi)容實(shí)現(xiàn)樹的結(jié)構(gòu),直接實(shí)現(xiàn)第二步抽象出來的接口即可睹限,如下:
struct BinaryTree<E: BinaryTreeCompare>: BinaryTreeProtocol {
var root: Child<E>?
var length: Int = 0
var binaryTreeTravelsal: BinaryTreeTraversal<E>?
typealias T = E
init() {
root = nil
}
init(travelsal: BinaryTreeTraversal<E>?) {
self.init()
self.binaryTreeTravelsal = BinaryTreeTraversal<E>()
}
mutating func add(element: E) {
if root == nil {
root = Child(element: element, left: nil, right: nil)
return
}
var child = root
var parent = root
var compareResult: BinaryTreeCompareResult?
while child != nil {
parent = child
if element > child!.element {
child = child?.right
compareResult = .BiggerThanRight
}else if element < child!.element {
child = child?.left
compareResult = .BiggerThanLeft
}else {
compareResult = .Equal
}
}
let childNode = Child(element: element, left: nil, right: nil)
if BinaryTreeCompareResult.BiggerThanLeft == compareResult {
parent?.left = childNode
length += 1
}else if BinaryTreeCompareResult.BiggerThanRight == compareResult {
parent?.right = childNode
length += 1
}
}
mutating func clear() {
root = nil
length = 0
}
mutating func remove(element: E) {
if root == nil { return }
var child = root
var parent = root
var compareResult: BinaryTreeCompareResult?
while child != nil {
if element > child!.element {
parent = child
child = child?.right
compareResult = .BiggerThanRight
}else if element < child!.element{
parent = child
child = child?.left
compareResult = .BiggerThanLeft
}else {
if compareResult == .BiggerThanRight {
parent?.right = nil
}else if compareResult == .BiggerThanLeft {
parent?.left = nil
}
length -= 1
break
}
}
}
func size() -> Int { length }
func isEmpty() -> Bool { length == 0 }
func contains(element: E) -> Bool {
var child = root
while child != nil {
if element > child!.element {
child = child!.left
}else if element < child!.element {
child = child!.right
}else {
return true
}
}
return false
}
var description: String {""}
}
二叉樹的遍歷
二叉樹的遍歷方式有以下幾種:
- 前序遍歷:
頭節(jié)點(diǎn)
先遍歷譬猫,再依次遍歷左子樹
與右子樹
- 中序遍歷:
左子樹
先遍歷,再遍歷頭節(jié)點(diǎn)
邦泄,最后遍歷右子樹
- 后序遍歷:
左子樹
先遍歷删窒,再依次遍歷右子樹
裂垦,最后遍歷頭節(jié)點(diǎn)
- 層序遍歷:按照
二叉樹的層級
一層一層的往下遍歷(這個在實(shí)現(xiàn)樹的深度
顺囊,以及判斷是否是一棵完全二叉樹中
和查找中序遍歷的前驅(qū)節(jié)點(diǎn)與后繼節(jié)點(diǎn)
等中用到,很重要)
這里的 前序
蕉拢、中序
特碳、后序
是指節(jié)點(diǎn)的遍歷的先后順序。這幾種遍歷可以通過 遞歸
來實(shí)現(xiàn)晕换,而最后的 層序遍歷
午乓,在進(jìn)行每一層 遍歷
的時候,有著隊(duì)列的 先進(jìn)先出的特點(diǎn)
闸准,可以考慮將 節(jié)點(diǎn)
放入到隊(duì)列中益愈,然后通過隊(duì)列的 出隊(duì)
間接實(shí)現(xiàn)層級的遍歷,而終止條件就是 隊(duì)列的長度為空
的時候。
/*
二叉搜索樹遍歷的幾種方法:
1. 前序遍歷 (PreOrder Traversal)
2. 中序遍歷 (InOrder Traversal)
3. 后序遍歷(Postorder Traversal)
4. 層序遍歷(Level Order Traversal)
*/
class BinaryTreeTraversal<E : Equatable & Comparable> {
/*
前序遍歷
*/
func preOrderTraversal(root: Child<E>?) {
if root == nil {
return
}
print(root!.element,terminator: " ")
// print(_ items: Any..., separator: String = " ", terminator: String = "\n")
preOrderTraversal(root: root?.left)
preOrderTraversal(root: root?.right)
}
/*
中序遍歷
*/
func inOrderTraversal(root: Child<E>?) {
if root == nil {
return
}
inOrderTraversal(root: root?.left)
print(root!.element,terminator: " ")
inOrderTraversal(root: root?.right)
}
/*
后序遍歷
*/
func postOrderTraversal(root: Child<E>?) {
if root == nil {
return
}
postOrderTraversal(root: root?.left)
postOrderTraversal(root: root?.right)
print(root!.element,terminator: " ")
}
/*
層序遍歷(一層一層遍歷)
*/
func levelOrderTraversal(root: Child<E>) {
var queue = SignalQueue<Child<E>>()
queue.enQueue(element: root)
var travelsalStr = "["
while !queue.isEmpty() {
let node = queue.deQueue()
travelsalStr.append("\(node!.element) ")
if let left = node?.left {
queue.enQueue(element: left)
}
if let right = node?.right {
queue.enQueue(element: right)
}
}
travelsalStr.removeLast()
travelsalStr.append("]")
print("\(travelsalStr)")
}
/*
層序遍歷(一層一層遍歷)
*/
func levelOrderTraversalCustom(root: Child<E>, enumtorEachClosure: (Child<E>)-> Bool) {
var queue = SignalQueue<Child<E>>()
queue.enQueue(element: root)
while !queue.isEmpty() {
let node = queue.deQueue()
if enumtorEachClosure(node!) {
return
}
if let left = node?.left {
queue.enQueue(element: left)
}
if let right = node?.right {
queue.enQueue(element: right)
}
}
}
}
關(guān)于搜索二叉樹的節(jié)點(diǎn)刪除(刪除判斷依據(jù)是按照該節(jié)點(diǎn)的度進(jìn)行相應(yīng)的調(diào)整)
二叉樹的節(jié)點(diǎn)刪除不要理解為是節(jié)點(diǎn)連同節(jié)點(diǎn)的左右子樹一起刪除蒸其,二叉樹的節(jié)點(diǎn)刪除是僅僅刪除某個節(jié)點(diǎn)敏释,但是刪除節(jié)點(diǎn)的孩子節(jié)點(diǎn)還需要保留。所以針對于要刪除的節(jié)點(diǎn)分為三種情況:
- 擁有兩個孩子節(jié)點(diǎn)的情況(度為2)
- 擁有一個孩子節(jié)點(diǎn)的情況(度為1)
- 刪除的節(jié)點(diǎn)是葉子結(jié)點(diǎn)(度為0)
- 刪除的節(jié)點(diǎn)是根節(jié)點(diǎn)且只有一個子樹
針對于三種情況下的刪除摸袁,每種情況單獨(dú)分析:
- 擁有兩個孩子節(jié)點(diǎn)的情況
刪除該節(jié)點(diǎn)之后由于沒有了以當(dāng)前節(jié)點(diǎn)為樹的 根節(jié)點(diǎn)
钥顽,所以需要找一個節(jié)點(diǎn)去 取代當(dāng)前節(jié)點(diǎn)的位置
,由于 二叉搜索樹
有順序大小靠汁,所以按照搜索樹的特點(diǎn)蜂大,刪除節(jié)點(diǎn)之后,這個順序依然能夠保持蝶怔。
依據(jù)這個特點(diǎn)奶浦,可以通過二叉搜索樹的 前驅(qū)節(jié)點(diǎn)
以及 后繼節(jié)點(diǎn)
來替換要 刪除的節(jié)點(diǎn)
,同時能夠保證其正常替代節(jié)點(diǎn)的功能添谊。
- 擁有一個孩子節(jié)點(diǎn)的情況
如果 刪除的節(jié)點(diǎn)node
只擁有一個孩子節(jié)點(diǎn)财喳,需要用 子節(jié)點(diǎn)child
去替換刪除的 節(jié)點(diǎn)node
。這里要區(qū)分孩子 節(jié)點(diǎn)是左節(jié)點(diǎn)
還是 右節(jié)點(diǎn)
- 如果
child
是左節(jié)點(diǎn)
child.parent = node.parent
node.parent.left = child
- 如果
child
是右節(jié)點(diǎn)
child.parent = node.parent
node.parent.right = child
- 刪除的節(jié)點(diǎn)是葉子節(jié)點(diǎn)
如果刪除的節(jié)點(diǎn)是葉子結(jié)點(diǎn)斩狱,可以直接進(jìn)行刪除
node.parent = nil
- 刪除的是根節(jié)點(diǎn)
root = child
child.parent = nil
綜合上面的所有情況耳高,可以實(shí)現(xiàn)刪除方法
mutating func deleteNode(node: inout Child<E>){
//含有兩個孩子(度為2的情況)
if node.hasTwoChild() {
if let behindNode = behindChildInOrderTree(node) {
//放入到根節(jié)點(diǎn)
node.element = behindNode.element
//指向當(dāng)前的后繼節(jié)點(diǎn)
node = behindNode
}
}
//節(jié)點(diǎn)度為1或者0的情況
let replaceNode = node.left != nil ? node.left : node.right
if replaceNode != nil {
//更換刪除的子節(jié)點(diǎn)的父節(jié)點(diǎn)為刪除子節(jié)點(diǎn)的父節(jié)點(diǎn)
replaceNode?.parent = replaceNode?.parent
if node.parent == nil{
root = replaceNode
replaceNode?.parent = nil
}else if node == replaceNode?.parent?.left {
replaceNode?.parent?.left = replaceNode
}else if node == replaceNode?.parent?.right{
replaceNode?.parent?.right = replaceNode
}
}
//葉子節(jié)點(diǎn)
else {
if node == node.parent?.left {
node.parent?.left = nil
}else {
node.parent?.right = nil
}
}
}
如何判斷一棵樹是否為完全二叉樹
完全二叉樹
的特點(diǎn):可以理解為 滿二叉樹
多一層不滿的情況,同時 滿二叉樹
也是一棵 完全二叉樹
。通過 層序遍歷
樹的節(jié)點(diǎn)來完成判定:
- 當(dāng)前節(jié)點(diǎn)有兩個孩子節(jié)點(diǎn),繼續(xù)向后遍歷
- 當(dāng)前節(jié)點(diǎn)不含有左孩子只含有右孩子庆揪,不是完全二叉樹膀捷,直接返回
- 當(dāng)前節(jié)點(diǎn)
不含有左孩子,也不含有右孩子
缺前,遍歷后序節(jié)點(diǎn),后序節(jié)點(diǎn)需要滿足均為葉子節(jié)點(diǎn)才能滿足其是完全二叉樹
- 當(dāng)前節(jié)點(diǎn)
含有左孩子,不含有右孩子
修壕,遍歷后序節(jié)點(diǎn),后序節(jié)點(diǎn)需要滿足均為葉子節(jié)點(diǎn)才能滿足其是完全二叉樹
按照判定標(biāo)準(zhǔn)遏考,可以實(shí)現(xiàn)其內(nèi)容:
/*
通過直接判斷當(dāng)前節(jié)點(diǎn)是否滿足情況慈鸠,正常方案
*/
func compeleteTreeNormalMethod(root: Child<Int>) -> Bool {
var travelsalQueue = SignalQueue<Child<Int>>()
travelsalQueue.enQueue(element: root)
var isJudgeLeaf = false
while !travelsalQueue.isEmpty() {
let child = travelsalQueue.deQueue()
//判斷是否是葉子節(jié)點(diǎn)
if isJudgeLeaf && !child!.isLeaf() {
return false
}
//正常節(jié)點(diǎn),繼續(xù)遍歷下面的節(jié)點(diǎn)
if child!.hasTwoChild(){
travelsalQueue.enQueue(element: root.left!)
travelsalQueue.enQueue(element: root.right!)
}else if child?.left == nil && child?.right != nil {
return false
}else {
//注意當(dāng)前如果左孩子不為空灌具,需要將左孩子加入到隊(duì)列以全部節(jié)點(diǎn)能夠正常遍歷
if child?.left != nil {
travelsalQueue.enQueue(element: child!.left!)
}
isJudgeLeaf = true
}
}
return true
}
以上的方案可以優(yōu)化一下青团,可以直接 判定節(jié)點(diǎn)的左右孩子
進(jìn)而保證只需要依次遍歷左右孩子就能完成一次判定
,不會像上面的方案 重復(fù)多次判定左右孩子節(jié)點(diǎn)為空的操作
咖楣。
func isCompeleteTree(root: Child<Int>) -> Bool {
var travelsalQueue = SignalQueue<Child<Int>>()
travelsalQueue.enQueue(element: root)
var judgeIsLeaf = false
while !travelsalQueue.isEmpty() {
let child = travelsalQueue.deQueue()
if judgeIsLeaf && !child!.isLeaf() {
return false
}
if child?.left != nil {
travelsalQueue.enQueue(element: child!.left!)
}else if child?.right != nil{
return false
}
//左節(jié)點(diǎn)為nil
if child?.right != nil {
travelsalQueue.enQueue(element: child!.right!)
}else{
//此時包含兩種情況:1.左節(jié)點(diǎn)有值 2.左節(jié)點(diǎn)沒有值督笆,在這兩種情況下,只需要判斷后序的節(jié)點(diǎn)是否是葉子節(jié)點(diǎn)即可
//包含兩種情況:1. 左節(jié)點(diǎn)為nil 右節(jié)點(diǎn)為nil 2. 左節(jié)點(diǎn)非nil 诱贿,右節(jié)點(diǎn)nil 該節(jié)點(diǎn)為分界節(jié)點(diǎn)娃肿,后序節(jié)點(diǎn)需要滿足均為葉子節(jié)點(diǎn)的特征
judgeIsLeaf = true
}
//經(jīng)過一次循環(huán)遍歷之后,此時已經(jīng)過濾如下情況:
//1. 左節(jié)點(diǎn)為nil、右節(jié)點(diǎn)含有child
//2. 左節(jié)點(diǎn)有值料扰,右節(jié)點(diǎn)含有值
//3. 左節(jié)點(diǎn)與右節(jié)點(diǎn)同時都沒有值锨阿。
}
return true
}
如何翻轉(zhuǎn)二叉樹
二叉樹的翻轉(zhuǎn)是左右子樹的元素節(jié)點(diǎn)進(jìn)行交換
,如下:
50
/ \
30 60
/ \ \
20 40 70
/ \
10 25
前序: 50, 30, 20, 10, 25, 40, 60, 70
中序: 10, 20, 25, 30, 40, 50, 60, 70
后序: 10, 25, 20, 40, 30, 70, 60, 50
=========== 翻轉(zhuǎn)結(jié)果 ===========
50
/ \
60 30
/ / \
70 40 20
/ \
25 10
思想:
翻轉(zhuǎn)是 交換左子樹與右子樹
的位置记罚,所以通過 遍歷來交換兩者的位置
即可墅诡,
遍歷的方法有 前序遍歷、中序遍歷桐智、后序遍歷以及層序遍歷
末早,都是能滿足條件的
/*
前序遍歷實(shí)現(xiàn)
*/
func invertTreeUsePreOrder(root: Child<Int>?) {
if root == nil { return }
let tmp = root?.left
root?.left = root?.right
root?.right = tmp
invertTreeUsePreOrder(root: root!.left)
invertTreeUsePreOrder(root: root!.right)
}
/*
后序遍歷實(shí)現(xiàn)
*/
func invertTreeUsePostOrder(root: Child<Int>?) {
if root == nil { return }
invertTreeUsePostOrder(root: root?.left)
invertTreeUsePostOrder(root: root?.right)
let tmp = root?.left
root?.left = root?.right
root?.right = tmp
}
/*
中序遍歷實(shí)現(xiàn)
注意中序遍歷的時候,右子樹再進(jìn)行子節(jié)點(diǎn)交換前说庭,同級別的節(jié)點(diǎn)先交換了左右子樹然磷,導(dǎo)致右子樹變成了左子樹,所以此種情況下應(yīng)該依舊交換左子樹內(nèi)容
*/
func invertTreeUseInOrder(root: Child<Int>?) {
if root == nil { return }
invertTreeUseInOrder(root: root?.left)
let tmp = root?.left
root?.left = root?.right
root?.right = tmp
invertTreeUseInOrder(root: root?.left)
}
/*
使用層序遍歷來完成二叉樹的翻轉(zhuǎn)
*/
func invertTreeUseLevel(root: Child<Int>) {
var travelQueue = SignalQueue<Child<Int>>()
travelQueue.enQueue(element: root)
while !travelQueue.isEmpty() {
let child = travelQueue.deQueue()
let tmp = child?.left
child?.left = child?.right
child?.right = tmp
if child?.left != nil {
travelQueue.enQueue(element: child!.left!)
}
if child?.right != nil {
travelQueue.enQueue(element: child!.right!)
}
}
}
如何依據(jù)遍歷結(jié)果重構(gòu)一棵二叉樹
能夠根據(jù)遍歷結(jié)果來重新構(gòu)建一棵唯一的二叉樹的刊驴,有以下兩種情況:
- 中序遍歷 + 前序遍歷
- 中序遍歷 + 后序遍歷
特點(diǎn)就是兩種都必須有 中序遍歷
姿搜, 中序遍歷
是用來確定左右子樹的,所以對于重構(gòu)唯一的二叉樹是必不可少的捆憎。由于 前序遍歷 + 后序遍歷
結(jié)果組合是無法確定 左右子樹
的舅柜,所以也就不能夠確定唯一的 二叉樹
比如:
前序: 50, 30, 20, 10, 25, 40, 60, 70
中序: 10, 20, 25, 30, 40, 50, 60, 70
根據(jù)前序順序,確定根節(jié)點(diǎn)為 50躲惰,在中序遍歷中找到根節(jié)點(diǎn)位置致份,中序遍歷中,50左邊的節(jié)點(diǎn)為 左子樹節(jié)點(diǎn)
础拨,右邊的節(jié)點(diǎn)為 右子樹節(jié)點(diǎn)
氮块,然后依據(jù)前序遍歷的第二個節(jié)點(diǎn)30得出中序遍歷30的位置,確定 50左子樹的根節(jié)點(diǎn)為30
诡宗,循環(huán)查找構(gòu)建出一棵完整的樹滔蝉。
50
/ \
30 60
/ \ \
20 40 70
/ \
10 25
二叉樹的前驅(qū)節(jié)點(diǎn)與后繼節(jié)點(diǎn)查找
二叉樹的 前驅(qū)節(jié)點(diǎn)
是針對中序遍歷順序來說的,指的 中序遍歷
的節(jié)點(diǎn)的 前一個節(jié)點(diǎn)
塔沃。有著以下幾個特點(diǎn):
- 如果存在左子樹蝠引,前驅(qū)節(jié)點(diǎn)就是左子樹最后一個遍歷的節(jié)點(diǎn)(
while child != nil { child.left.right.right }
) - 如果不存在左子樹,那么前驅(qū)節(jié)點(diǎn)是按照當(dāng)前節(jié)點(diǎn)的層層父節(jié)點(diǎn)往上找芳悲,直到當(dāng)前的節(jié)點(diǎn)為父節(jié)點(diǎn)的右子樹節(jié)點(diǎn)的時候立肘,那么往上找到的節(jié)點(diǎn)的父節(jié)點(diǎn)就是前驅(qū)節(jié)點(diǎn)(
while child.parent.right != child { child = child.parent }
)
二叉樹的 后繼節(jié)點(diǎn)
是中序遍歷節(jié)點(diǎn)的 后一個節(jié)點(diǎn)
后繼節(jié)點(diǎn)
幾點(diǎn)重要特性(與前驅(qū)節(jié)點(diǎn)恰恰相反):
- 如果存在右子樹边坤,那么
右子樹的最左邊的child
就是后繼節(jié)點(diǎn) - 如果
不存在右子樹
名扛,直接找父節(jié)點(diǎn),直到父節(jié)點(diǎn)的左孩子節(jié)點(diǎn)是當(dāng)前節(jié)點(diǎn)
的時候茧痒,那么該父節(jié)點(diǎn)就是其后繼節(jié)點(diǎn)
結(jié)合上面的內(nèi)容肮韧,可以大致實(shí)現(xiàn)其方法:
func preChildInOrderTree(_ target: Child<Int>) -> Child<Int>? {
var child = target
if child.left != nil {
child = child.left!
while child.right != nil {
child = child.right!
}
return child
}
//沒有左子樹
else {
while child.parent != nil && child.parent?.right != child {
child = child.parent!
}
return child.parent
}
}
func behindChildInOrderTree(_ target: Child<Int>) -> Child<Int>? {
var child = target
if child.right != nil {
child = child.right!
while child.left != nil {
child = child.left!
}
return child
}
//沒有右子樹
else {
while child.parent != nil && child.parent?.left != child {
child = child.parent!
}
return child.parent
}
}