最近開始學習go,公司也安排了一些對應的練習題,其中就包括二叉樹相關的練習題,剛好也順便撿起這方面的知識點
在計算機資料結構中,我們會經常接觸到樹這種典型的非線性資料結構,下面圖片展示的就是一個標準的樹結構
在資料結構中,樹的定義如下。
樹(tree)是n(n>=0)個節點的有限集。當n=0時,稱為空樹。在任意一個非空樹中,有如下特點。
- 有且僅有一個特定的稱為根的節點
- 當n>1時,其餘節點可分為m(m>0)個互不相交的有限集,每一個集合本身又是一個樹,並稱為根的子樹
什麼是二叉樹
二叉樹(binary tree)是樹的一種特性形式。二叉,顧名思義,這種樹的每個節點最多有2個位元組點。注意,這裡是最多有2個,也可能只有1個,或者沒有孩子節點。
二叉樹還有二種特殊形式,一個叫作滿二叉樹,另一個叫作完全二叉樹
一個二叉樹的所有非葉子節點都存在左右節點,並且所有葉子節點都在同一層級上,那麼這個樹就是滿二叉樹。如下圖所示
對一個有n個節點的二叉樹,按層級順序編號,則所有節點的編號為1到n。如果這個樹所有節點和同樣深度的滿二叉樹的編號為1到n的節點位置相同,則這個二叉樹為完全二叉樹。如下圖所示
二叉樹的遍歷
介紹完二叉樹的概念,開始進入實踐,二叉樹是典型的非線性資料結構,遍歷時需要把非線性關聯的節點轉化成一個線性的序列,以不同的方式來遍歷,遍歷出的序列順序也不同。
從節點之間位置關係的角度來看,二叉樹的遍歷分為4種。
1.前序遍歷
2.中順遍歷
3.後序遍歷
4.層序遍歷
從更宏觀的角度來看,二叉樹的遍歷歸結為兩大類
1.深度優先遍歷(前序遍歷,中序遍歷,後序遍歷)
2.廣度優先遍歷(層序遍歷)
深度優先遍歷
1.前序遍歷
二叉樹的前序遍歷,輸出順序是根節點、左子樹、右子樹。如下圖所示
程式碼實現如下
package main
import (
"fmt"
)
//定義二叉樹結構體
type binaryTree struct {
value int
leftNode *binaryTree
rightNode *binaryTree
}
func main(){
//0代表節點為空
num := []int{1,2,3,4,5,0,6}
//將陣列切片轉化為二叉樹結構體
tree := createBinaryTree(0,num)
//二叉樹資料
var front []int
front = frontForeach(*tree,front)
fmt.Println(front)
}
//建立二叉樹
func createBinaryTree(i int,nums []int) *binaryTree {
tree := &binaryTree{nums[i], nil, nil}
//左節點的陣列下標為1,3,5...2*i+1
if i<len(nums) && 2*i+1 < len(nums) {
tree.leftNode = createBinaryTree(2*i+1, nums)
}
//右節點的陣列下標為2,4,6...2*i+2
if i<len(nums) && 2*i+2 < len(nums) {
tree.rightNode = createBinaryTree(2*i+2, nums)
}
return tree
}
//前序遍歷
func frontForeach(tree binaryTree,num []int) []int{
//先遍歷根節點
if tree.value != 0 {
num = append(num,tree.value)
}
var leftNum,rightNum []int
//若存在左節點,遍歷左節點樹
if tree.leftNode != nil {
leftNum = frontForeach(*tree.leftNode,leftNum)
for _,value := range leftNum{
num = append(num,value)
}
}
//若存在右節點,遍歷右節點樹
if tree.rightNode != nil {
rightNum = frontForeach(*tree.rightNode,rightNum)
for _,value := range rightNum{
num = append(num,value)
}
}
return num
}
2.中序遍歷
二叉樹的中序遍歷,輸出順序是左子樹、根節點、右子樹。如下圖所示
package main
import (
"fmt"
)
//定義二叉樹結構體
type binaryTree struct {
value int
leftNode *binaryTree
rightNode *binaryTree
}
func main(){
//0代表節點為空
num := []int{1,2,3,4,5,0,6}
tree := createBinaryTree(0,num)
//二叉樹資料
var middle []int
middle = middleForeach(*tree,middle)
fmt.Println(middle)
}
//建立二叉樹
func createBinaryTree(i int,nums []int) *binaryTree {
tree := &binaryTree{nums[i], nil, nil}
//左節點的陣列下標為1,3,5...2*i+1
if i<len(nums) && 2*i+1 < len(nums) {
tree.leftNode = createBinaryTree(2*i+1, nums)
}
//右節點的陣列下標為2,4,6...2*i+2
if i<len(nums) && 2*i+2 < len(nums) {
tree.rightNode = createBinaryTree(2*i+2, nums)
}
return tree
}
//中序遍歷
func middleForeach(tree binaryTree,num []int) []int{
var leftNum,rightNum []int
//若存在左節點,遍歷左節點樹
if tree.leftNode != nil {
leftNum = middleForeach(*tree.leftNode,leftNum)
for _,value := range leftNum{
num = append(num,value)
}
}
//先遍歷根節點
if tree.value != 0 {
num = append(num,tree.value)
}
//若存在右節點,遍歷右節點樹
if tree.rightNode != nil {
rightNum = middleForeach(*tree.rightNode,rightNum)
for _,value := range rightNum{
num = append(num,value)
}
}
return num
}
3.後序遍歷
二叉樹的後序遍歷,輸出順序是左子樹、右子樹、根節點。如下圖所示
package main
import (
"fmt"
)
//定義二叉樹結構體
type binaryTree struct {
value int
leftNode *binaryTree
rightNode *binaryTree
}
func main(){
//0代表節點為空
num := []int{1,2,3,4,5,0,6}
tree := createBinaryTree(0,num)
//二叉樹資料
var behind []int
behind = behindForeach(*tree,behind)
fmt.Println(behind)
}
//建立二叉樹
func createBinaryTree(i int,nums []int) *binaryTree {
tree := &binaryTree{nums[i], nil, nil}
//左節點的陣列下標為1,3,5...2*i+1
if i<len(nums) && 2*i+1 < len(nums) {
tree.leftNode = createBinaryTree(2*i+1, nums)
}
//右節點的陣列下標為2,4,6...2*i+2
if i<len(nums) && 2*i+2 < len(nums) {
tree.rightNode = createBinaryTree(2*i+2, nums)
}
return tree
}
//後序遍歷
func behindForeach(tree binaryTree,num []int) []int{
var leftNum,rightNum []int
//若存在左節點,遍歷左節點樹
if tree.leftNode != nil {
leftNum = behindForeach(*tree.leftNode,leftNum)
for _,value := range leftNum{
num = append(num,value)
}
}
//若存在右節點,遍歷右節點樹
if tree.rightNode != nil {
rightNum = behindForeach(*tree.rightNode,rightNum)
for _,value := range rightNum{
num = append(num,value)
}
}
//先遍歷根節點
if tree.value != 0 {
num = append(num,tree.value)
}
return num
}
以上三種遍歷方式相對比較簡單,只需要的遍歷過程中調整遍歷順序即可完成,下面我們看看另一種方式的遍歷
廣度優先遍歷
層序遍歷
顧名思義,就是二叉樹按照從根節點到葉子節點的層次關係,一層一層橫向遍歷各個節點,如下所示
程式碼實現上,與深度優先遍歷有所區別,沒有使用遞迴的方式去實現,而是使用了佇列的方式,遍歷過程中,透過新增佇列,讀取隊頭的方式,完成層序遍歷
package main
import (
"fmt"
)
//定義二叉樹結構體
type binaryTree struct {
value int
leftNode *binaryTree
rightNode *binaryTree
}
func main(){
//0代表節點為空
num := []int{1,2,3,4,5,0,6}
tree := createBinaryTree(0,num)
//二叉樹資料
var row []int
row = rowForeach(*tree,row)
fmt.Println(row)
}
//建立二叉樹
func createBinaryTree(i int,nums []int) *binaryTree {
tree := &binaryTree{nums[i], nil, nil}
//左節點的陣列下標為1,3,5...2*i+1
if i<len(nums) && 2*i+1 < len(nums) {
tree.leftNode = createBinaryTree(2*i+1, nums)
}
//右節點的陣列下標為2,4,6...2*i+2
if i<len(nums) && 2*i+2 < len(nums) {
tree.rightNode = createBinaryTree(2*i+2, nums)
}
return tree
}
//層序遍歷
func rowForeach(tree binaryTree,num []int) []int {
//定義佇列
var queue []binaryTree
queue = append(queue,tree)
for len(queue) > 0{
var first binaryTree
first = queue[0]
if first.value != 0 {
//取出隊頭值
num = append(num,first.value)
}
//將隊頭刪除
queue = queue[1:]
//左節點存在則加入隊尾
if first.leftNode != nil {
queue = append(queue,*first.leftNode)
}
//右節點存在則加入隊尾
if first.rightNode != nil {
queue = append(queue,*first.rightNode)
}
}
return num
}
二叉堆
二叉堆本質上是一種完全二叉樹,它分為兩個型別
1.最大堆:大頂堆任何一個父節點,都大於或等於它左、右孩子節點的值,如下圖所示
2.最小堆:小頂堆的任何一個父節點,都小於或等於它左、右孩子節點的值,如下圖所示
二叉堆的根節點叫作堆頂。最大堆和最小堆的特點決定了:最大堆的堆頂是整個堆中的最大元素;最小堆的堆頂事整個堆中的最小元素。
對於二叉堆,有如下幾種操作
1.插入節點
2.刪除節點
3.構建二叉樹
在程式碼實現上,由於堆是一個完全二叉樹,所以在儲存上可以使用陣列實現,假設父節點的下標為parent,那麼它的左孩子下標就是2 * parent+1,右孩子下標就是2 * parent+2
構建最大堆程式碼如下:實現思路大致先找到最近的非葉子結點,假設有n個結點,則最近的非葉子結點為n/2,對每個結點進行判斷,判斷結點是否大於根節點,若不大於則節點下沉,這樣一步步的迴圈判斷,就構建了一個最大堆
package main
import "fmt"
func main(){
var numArr = []int{2,8,23,4,5,77,65,43}
buildMaxHeap(numArr,len(numArr))
fmt.Print(numArr)
}
//構建最大堆
func buildMaxHeap(arr []int,arrLen int){
for i := arrLen/2;i >= 0; i-- {
sink(arr,i,arrLen)
}
}
//樹節點值下沉判斷
func sink(arr []int,i,arrLen int) {
//左節點下標
left := i*2+1
//右節點下標
right := i*2+2
largest := i
//若左節點大於父節點,則交換值
if left < arrLen && arr[left] > arr[largest] {
largest = left
}
if right < arrLen && arr[right] > arr[largest] {
largest = right
}
//存在節點值大於父節點值,需要交換資料
if largest != i {
//交換值
swap(arr,i,largest)
//交換完成後,繼續判斷交換的節點值是否需要繼續下沉
sink(arr,largest,arrLen)
}
}
//交換位置
func swap(arr []int,i,j int) {
arr[i],arr[j] = arr[j],arr[i]
}
優先佇列
佇列的特點是先進先出(FIFO)
入佇列,將新元素置於隊尾;出佇列,隊頭元素最先被移出;
而優先佇列不再遵循先入先出的原則,而是分為兩種情況。
- 最大優先佇列,無論入隊順序如何,都是當前最大的元素優先出列
- 最小優先佇列,無論入隊順序如何,都是當前最小的元素優先出列
在上面實現了構建最大堆的程式碼後,還有兩個基本操作,插入節點和刪除節點,插入節點必須先往陣列最後插入,並進行判斷插入的值是否需要上浮到父節點,刪除節點只允許刪除根節點的值,刪除後再將陣列最後的值放置到根節點,再進行調整,完成節點的值下沉。當完成上述操作後,就實現了優先佇列
實現程式碼如下:
package main
import "fmt"
func main(){
var numArr = []int{2,8,23,4,5,77,65,43}
arrLen := len(numArr)
buildMaxHeapQueue(numArr,arrLen)
var out int
numArr,out = outQueue(numArr)
fmt.Print(numArr,out)
fmt.Print("\n")
numArr = inQueue(numArr,55)
fmt.Print(numArr)
fmt.Print("\n")
numArr,out = outQueue(numArr)
fmt.Print(numArr,out)
fmt.Print("\n")
}
//入佇列
func inQueue(arr []int,num int) []int {
arr = append(arr,num)
floatingNode(arr,len(arr)-1)
return arr
}
//出佇列
func outQueue(arr []int) ([]int,int){
first := arr[0]
arrLen := len(arr)
if arrLen == 0 {
panic("nothing")
}
if arrLen == 1 {
var emptyArr []int
return emptyArr,first
}
//交換最後一個節點和根節點值
swapNode(arr,0,arrLen-1)
//將切片最後的值刪除
arr = arr[0:arrLen-1]
//節點下沉
sinkNode(arr,0,len(arr))
return arr,first
}
//構建大頂堆
func buildMaxHeapQueue(arr []int,arrLen int){
for i := arrLen/2;i >= 0; i-- {
sinkNode(arr,i,arrLen)
}
}
//節點值上浮判斷
func floatingNode(arr []int,i int){
var root int
if i%2 == 0 {
root = i/2-1
}else {
root = i/2
}
//判斷父節點是否小於子節點
if arr[root] < arr[i] {
swapNode(arr,root,i)
if i > 1 {
floatingNode(arr,root)
}
}
}
//樹節點值下沉判斷
func sinkNode(arr []int,i int,arrLen int) {
//左節點下標
left := i*2+1
//右節點下標
right := i*2+2
largest := i
//若左節點大於父節點,則交換值
if left < arrLen && arr[left] > arr[largest] {
largest = left
}
if right < arrLen && arr[right] > arr[largest] {
largest = right
}
//存在節點值大於父節點值,需要交換資料
if largest != i {
//交換值
swapNode(arr,i,largest)
//交換完成後,繼續判斷交換的節點值是否需要繼續下沉
sinkNode(arr,largest,arrLen)
}
}
//交換位置
func swapNode(arr []int,i,j int) {
arr[i],arr[j] = arr[j],arr[i]
}
堆排序
堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。
其實現思想在於先構建一個最大堆,再重新遍歷,每次遍歷將根節點與最後一個元素值交換,然後隔離最後一個元素,因為此時最後一個元素為最大的元素,然後根節點的值進行下沉操作,再次形成最大堆,再重複進行操作,就完成了排序。
package main
import "fmt"
func main(){
var numArr = []int{2,8,23,4,5,77,65,43}
sort := heapSort(numArr)
fmt.Print(sort)
}
//堆排序
func heapSort(arr []int) []int{
arrLen := len(arr)
//構建大頂堆
buildMaxHeap(arr,arrLen)
for i := arrLen-1; i >= 0; i-- {
swap(arr,0,i)
arrLen -= 1
sink(arr,0,arrLen)
}
return arr
}
//構建大頂堆
func buildMaxHeap(arr []int,arrLen int){
for i := arrLen/2;i >= 0; i-- {
sink(arr,i,arrLen)
}
}
//樹節點值下沉判斷
func sink(arr []int,i,arrLen int) {
//左節點下標
left := i*2+1
//右節點下標
right := i*2+2
largest := i
//若左節點大於父節點,則交換值
if left < arrLen && arr[left] > arr[largest] {
largest = left
}
if right < arrLen && arr[right] > arr[largest] {
largest = right
}
//存在節點值大於父節點值,需要交換資料
if largest != i {
//交換值
swap(arr,i,largest)
//交換完成後,繼續判斷交換的節點值是否需要繼續下沉
sink(arr,largest,arrLen)
}
}
//交換位置
func swap(arr []int,i,j int) {
arr[i],arr[j] = arr[j],arr[i]
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結