1. 程式人生 > >二叉搜尋樹(二叉排序樹)BST與雙向列表的轉換

二叉搜尋樹(二叉排序樹)BST與雙向列表的轉換

目錄

1.二叉排序樹(Binary Sort Tree)

二叉排序樹又稱二叉查詢樹(Binary Search Tree) BST

二叉排序樹或者是一顆空樹,或者是滿足一下性質的一顆二叉樹:

  1. 若它的左子樹非空,則左子樹上所有結點的值均小於根結點的值;
  2. 若它的右子樹非空,則右子樹上所有結點的值均大於它的根結點的值;
  3. 它的左/右子樹分別為二叉排序樹

下面是初始序列建立二叉排序樹的過程:

{ 4, 2, 6, 1, 3, 5 }:

 

1.0 BST樹儲存結構

用二叉連結串列作為二叉排序樹的儲存結構!

typedef int KeyType;
typedef int ElemType;

/*二叉樹的結點儲存結構,二叉連結串列儲存結構*/
typedef struct BiTNode{
	KeyType data;
	int multiplicity;
	struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
/*
BiTNode:是結構型別
BiTree:是指向結點BiTNode的指標型別
*/

二叉排序樹的基本操作:

  1. 查詢key
  2. 插入結點
  3. 刪除結點

 1.1BST查詢

    /*在跟指標T所指的二叉排序樹中遞迴地查詢其關鍵字等於key的資料元素,
    若查詢成功,則指標p指向該資料元素結點,並返回true,否則指標p指向查詢路徑
    上訪問的最後一個結點並放回false,指標f指向T的雙親,其初始呼叫值為nullptr(空指標)*/


bool SearchBST(BiTree T, KeyType key, BiTree f, BiTree& p)
{
	/*在跟指標T所指的二叉排序樹中遞迴地查詢其關鍵字等於key的資料元素,
	若查詢成功,則指標p指向該資料元素結點,並返回true,否則指標p指向查詢路徑
	上訪問的最後一個結點並放回false,指標f指向T的雙親,其初始呼叫值為nullptr*/

	if (!T)
	{
		p = f;
		return false;
	}
	else if (key == T->data)
	{
		p = T;
		return true;
	}
	else if (key < T->data)
	{
		return SearchBST(T->lchild, key, T, p);
	}
	else if (key > T->data)
	{
		return SearchBST(T->rchild, key, T, p);
	}
}//SearchBST

1.2BST樹插入節點

插入前要先搜尋待插入的位置,如果存在值key值相同的節點,則不插入,並返回false。

    /*當二叉樹上不存在關鍵字等於e.key的資料元素時,插入e並返回true;
    否則返回false;*/

bool InsertBST(BiTree& T, ElemType e)
{
	/*當二叉樹上不存在關鍵字等於e.key的資料元素時,插入e並返回true;
	否則返回false;*/
	BiTree p;
	BiTree f = nullptr;
	if (!SearchBST(T, e, f, p))//指標p指向查詢路徑上訪問的最後一個結點並放回false
	{
		//BiTree s = (BiTree)malloc(sizeof(BiTNode));
		BiTree s = new BiTNode;
		s->data = e;
		s->multiplicity = 1;
		s->lchild = s->rchild = nullptr;
		if (!p)
		{//被插入結點為新的根結點!
			T = s;
		}
		else if (e < p->data)
		{//被插入結點*s為當前結點的左孩子
			p->lchild = s;
		}
		else if (e > p->data)
		{//被插入結點*s為當前結點的右孩子
			p->rchild = s;
		}
		return true;
	}
	else
	{//樹中已經有關鍵字相同的結點,不再插入,該結點的重複度+1
		p->multiplicity++;
		return false;
	}
}

1.3中序遍歷BST樹得到有序序列

void printVal(BiTree T)
{
	cout << T->data << " ";
}

void InOrderTraverseRecursive(BiTree T, void(*funcPtr)(BiTree))
{//對採用二叉連結串列表示的二叉樹的中序遍歷演算法
	/*中序遍歷二叉樹T的遞迴演算法!*/
	if (T)
	{
		InOrderTraverseRecursive(T->lchild, funcPtr);
		funcPtr(T);
		InOrderTraverseRecursive(T->rchild, funcPtr);
	}
	else
	{
		return;
	}
}

//中序遍歷二叉樹
/*E2版本的中序遍歷結構更加清晰、易懂!*/
void InOrderTraverseLoopE2(BiTree T, void(*funcPtr)(BiTree))
{/*二叉樹T採用二叉連結串列儲存結構,中序遍歷T的非遞迴演算法,對每一個元素呼叫funcPtr函式*/
	stack<BiTree> biTreeStack;//儲存結點指標的棧
	BiTree p;
	p = T;
	while (p || !biTreeStack.empty())
	{
		if (p)
		{//根指標進棧,然後遍歷左子樹
			biTreeStack.push(p);
			p = p->lchild;//這裡也是一直走到最下邊的結點
		}
		else
		{//根指標退棧,訪問根結點,然後遍歷右子樹
			p = biTreeStack.top();
			biTreeStack.pop();
			funcPtr(p);
			p = p->rchild;
		}
	}
}

1.4刪除BST樹上的一結點


bool Delete(BiTree& p)
{//從二叉排序樹中刪除結點p,並重接它的左或右子樹
	if (!p->rchild)
	{
		BiTree q = p;
		p = p->lchild;
		delete q;
	}
	else if (!p->lchild)
	{
		BiTree q = p;
		p = p->rchild;
		delete q;
	}
	else//左右子樹均不為空
	{
		BiTree q = p;
		BiTree s = p->lchild;
		while (s->rchild)
		{//
			q = s;
			s = s->rchild;
		}
		p->data = s->data;//s指向被刪除結點的前驅
		if (q != p)
		{
			q->rchild = s->lchild;//重接*q的右子樹
		}
		else
		{
			q->lchild = s->lchild;//重接*q的左子樹
		}
		delete s;
	}
	return true;
}


bool DeleteBSTKey(BiTree& T, KeyType key)
{/*若二叉排序樹T中存在關鍵字等於key的資料元素,則刪除該資料元素結點!*/
	if (!T) return false;
	else
	{
		if (key == T->data)
		{
			return Delete(T);
		}
		else if (key < T->data)
		{
			return DeleteBSTKey(T->lchild, key);
		}
		else if (key > T->data)
		{
			return DeleteBSTKey(T->rchild, key);
		}
	}
}//DeleteBST

1.5二叉排序樹記憶體的釋放_後序遍歷刪除每一個結點

bool deteteBiTNode(BiTree p)
{
	if (!p)
	{
		return false;
	}
	delete p;
	p = nullptr;
	return true;
}

void PostOrderTraverseRecursive(BiTree T, bool(*funcPtr)(BiTree))
{//對採用二叉連結串列表示的二叉樹的後序遍歷演算法
	/*後序遍歷二叉樹T的遞迴演算法!*/
	if (T)
	{
		PostOrderTraverseRecursive(T->lchild, funcPtr);
		PostOrderTraverseRecursive(T->rchild, funcPtr);

		funcPtr(T);
	}
	else
	{
		return;
	}
}

1.6二叉樹的後序遍歷_非遞迴演算法

參考我的另一篇博文:二叉樹_二叉連結串列儲存_前中後遍歷_棧:遞迴非遞迴遍歷_佇列:按層遍歷

後序遍歷的非遞迴演算法:以棧模擬遞迴的過程:
/*
二叉樹的後序遍歷--非遞迴實現
https://www.cnblogs.com/rain-lei/p/3705680.html

https://www.cnblogs.com/rain-lei/p/3705680.html
leetcode中有這麼一道題,非遞迴來實現二叉樹的後序遍歷。
二叉樹的後序遍歷順序為,root->left, root->right, root,
因此需要儲存根節點的狀態。顯然使用棧來模擬遞迴的過程,
但是難點是怎麼從root->right轉換到root。

方法1:判斷是否輪到棧頂p訪問法,設立剛訪問結點指標last
對於節點p可以分情況討論
1. p如果是葉子節點,直接訪問(輸出)
2. p如果有孩子,且孩子沒有被訪問過,則按照右孩子,左孩子的順序依次入棧
3. p如果有孩子,而且孩子都已經訪問過,則訪問p節點
如何來表示出p的孩是否都已經訪問過了呢?
最暴力的方法就是對每個節點的狀態進行儲存,
這麼做顯然是可以的,但是空間複雜度太大了。
我們可以儲存最後一個訪問的節點last,
如果滿足 (p->right==NULL && last ==p->left) || last=p->right,
那麼顯然p的孩子都訪問過了,接下來可以訪問p
*/

二叉樹的後序遍歷法1:區別棧頂結點p是否該訪問了之設立剛訪問結點指標法

void PostOrderTraverseE1(BiTree T, void(*funcPtr)(BiTree))
{
	if (!T)
		return;
	stack<BiTree> biTreeStack;//儲存結點指標的棧
	BiTree p = T;
	BiTree last = T;
	biTreeStack.push(p);
	while (!biTreeStack.empty())
	{
		p = biTreeStack.top();
		/*情況1:如果p是葉子結點,其左右子樹都為空,則可直接訪問p;
		情況2:如果滿足 (p->right==NULL && last ==p->left) || last=p->right,
		那麼顯然p的孩子都訪問過了,接下來可以訪問p
		如果p的右子樹為空,並且p的左子樹已經訪問過了,即(p->right==NULL && last ==p->left)
		那麼就可以訪問p了
		如果p的右子樹也訪問過了即last=p->right,也可以訪問p了
		
		*/
		if ((p->lchild == nullptr&&p->rchild == nullptr) || (p->rchild == nullptr&&last == p->lchild) || (last == p->rchild))
		{
			funcPtr(p);//訪問p
			last = p;//將剛才訪問的結點標記為p
			biTreeStack.pop();//p出棧
		}
		else
		{
			if (p->rchild)
			{//如果右子樹非空,則右子樹結點進棧
				biTreeStack.push(p->rchild);
			}
			if (p->lchild)
			{//如果左子樹非空,則左子樹結點進棧
				biTreeStack.push(p->lchild);
			}
		}
	}
}

二叉樹的後序遍歷法2:區別棧頂結點p是否該訪問了同一個結點兩次壓入兩次彈出法:

/*
法2:每個結點兩次壓入法
其實我們希望棧中儲存的從頂部依次是root->left, root->right, root,
當符合上面提到的條件時,就進行出棧操作。有一種巧妙的方法可以做到,
對於每個節點,都壓入兩遍,在迴圈體中,每次彈出一個節點賦給p,
如果p仍然等於棧的頭結點,說明p的孩子們還沒有被操作過,
應該把它的孩子們加入棧中,否則,訪問p。
也就是說,第一次彈出,將p的孩子壓入棧中,第二次彈出,訪問p。
*/

void PostOrderTraverseE2(BiTree T, void(*funcPtr)(BiTree))
{
	if (T == NULL) return;
 
	BiTree p = T;
	stack<BiTree> sta;
	sta.push(p);
	sta.push(p);
	while (!sta.empty())
	{
		p = sta.top(); 
		sta.pop();
		if (!sta.empty() && p == sta.top())
		{
			if (p->rchild) sta.push(p->rchild), sta.push(p->rchild);//C:逗號,運算子
			if (p->lchild) sta.push(p->lchild), sta.push(p->lchild);
		}
		else
		{
			funcPtr(p);
		}
	}
}

2.將BST轉換成雙向列表

參考《劍指offer》P191

面試題36:二叉搜尋樹與雙向列表

題目:輸入一顆二叉搜尋樹,將該二叉搜尋樹轉換成一個排序有序的雙向列表。要求不能建立新的結點,只能通過調整樹中結點的指標,(沒有此要求的情況下,當然可以中序遍歷BST然後建立新的雙向連結串列結點,形參有序的雙向列表。


void ConvertNode(BiTNode* pNode, BiTNode** pLastNodeInList)
{
	if (pNode == nullptr)
	{
		return;
	}
	BiTNode* pCurrent = pNode;

	if (pCurrent->lchild != nullptr)
	{
		ConvertNode(pCurrent->lchild, pLastNodeInList);
	}

	pCurrent->lchild = *pLastNodeInList;
	if (*pLastNodeInList != nullptr)
	{
		(*pLastNodeInList)->rchild = pCurrent;
	}

	*pLastNodeInList = pCurrent;

	if (pCurrent->rchild != nullptr)
	{
		ConvertNode(pCurrent->rchild, pLastNodeInList);
	}

}

BiTNode* Convert(BiTNode* pRootOfTree)
{
	BiTNode* pLastNodeInList = nullptr;
	ConvertNode(pRootOfTree, &pLastNodeInList);

	//pLastNodeInList指向雙向列表的尾巴結點
	//需求求得該雙向列表的頭結點
	BiTNode* pHeadOfList = pLastNodeInList;
	while (pHeadOfList != nullptr && pHeadOfList->lchild != nullptr)
	{//向左走到雙向列表的頭
		pHeadOfList = pHeadOfList->lchild;
	}
	return pHeadOfList;
}

2.1BST轉換成雙向列表測試:

#include "stdafx.h"
#include<stack>
#include<iostream>

using namespace std;

typedef int KeyType;
typedef int ElemType;

/*二叉樹的結點儲存結構,二叉連結串列儲存結構*/
typedef struct BiTNode{
	KeyType data;
	int multiplicity;
	struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
/*
BiTNode:是結構型別
BiTree:是指向結點BiTNode的指標型別
*/

int _tmain(int argc, _TCHAR* argv[])
{
	int MyArray[6] = { 4, 2, 6, 1, 3, 5 };

	BiTree T=nullptr;

	for (int i = 0; i < 6; i++)
	{
		InsertBST(T, MyArray[i]);
	}

	InOrderTraverseLoopE2(T, printVal);//1 2 3 4 5 6

	//bug!   2018年9月12日bugfixed!
	BiTNode* p = Convert(T);
	cout << endl;
	while (p != nullptr )
	{//向左走到雙向列表的頭
		cout << p->data << " ";
		p = p->rchild;
	}
	cout << endl;
	system("pause");
	return 0;
}
/*
輸出:
1 2 3 4 5 6
1 2 3 4 5 6
請按任意鍵繼續. . .
*/

3.BST轉雙向列表變種題

將二叉搜尋樹轉換成一個遞增的排序的雙向列表:

根據任意輸入建立一顆二叉搜尋樹,第一個原始是根結點原始,且元素可重複(通過在結點中設定元素的重複度multiplicity)

在轉換過程中不能建立新結點,只能調整指標,轉換完成後從頭到尾巴列印雙向列表:

二叉排序樹BST 二叉樹的遍歷 BST轉雙向列表