1. 程式人生 > >非遞迴,不用棧實現二叉樹中序遍歷

非遞迴,不用棧實現二叉樹中序遍歷

  最近總有人問這個問題:“如何不用棧,也不用遞迴來實現二叉樹的中序遍歷”。這個問題的實現就是迭代器問題,無論是Java還是C++,利用迭代器遍歷樹節點(Java中是TreeMap類,C++中是map類)都使用了中序遍歷,且無法使用遞迴和棧,演算法效率近似為O(1),不可能每個節點只訪問一次。

  純C實現的辦法很簡單,先定義型別。

// 定義byte_t型別
typedef unsigned char byte_t;

// 定義bool型別
typedef unsigned char bool;
#define true  1
#define false 0
// 定義用於比較的函式指標型別
typedef int(*treeset_compare_t)(const void*, const void*);

  下面結構體中的data[0]是C語言一種特殊用法,data[0]本身不表示任何大小,可以看作是一個指標,表明該結構體在記憶體中佔據的實際大小會超過結構體本身的位元組數,這樣,data指標就自動指向結構體中多餘的空間,該空間可以用來儲存節點值。並不是所有的C編譯器都能這麼做,但我測試過好像就VC++6不行,這段程式碼在GCC下編譯通過。

/**
 * 定義二叉樹節點型別
 */
typedef struct _btree_node
{
	struct _btree_node* parent;	// 指向父節點的指標
	struct _btree_node* lchild;	// 指向左孩子的指標
	struct _btree_node* rchild;	// 指向右孩子的指標
	struct _treeset* owner;	// 節點所屬的樹
	byte_t data[0];
} btree_node;
/**
 * 定義二叉樹結構體
 */
typedef struct _treeset
{
	struct _btree_node* root;	// 二叉樹根節點
	size_t count;	// 二叉樹節點數量
	size_t elemsize;
	treeset_compare_t compare;
} treeset;

  定義了以上型別,就可以進一步完成二叉樹初始化以及節點新增,刪除,查詢等函數了,函式宣告如下。

/**
 * 初始化二叉樹結構體
 * @param tree 指向二叉樹結構體的指標
 * @param elemsize 每個元素的大小
 * @param comp 用於比較的函式指標
 */
void treeset_init(treeset* tree, size_t elemsize, treeset_compare_t comp);

/**
 * 釋放二叉樹佔據的空間
 * @param tree 指向二叉樹結構體的指標
 */
 void treeset_free(treeset* tree);

/**
 * 向二叉樹中新增元素
 * @param tree 指向二叉樹結構體的指標
 * @param value 指向要新增元素的指標
 * @return 是否實際添加了節點
 */
bool treeset_add(treeset* tree, const void* value);

/**
 * 從二叉樹中刪除一個元素
 * @param tree 指向二叉樹結構體的指標
 * @param value 要刪除的節點內容
 * @return 是否刪除了節點
 */
bool treeset_remove(treeset* tree, const void* value);

/**
 * 在二叉樹中查詢一個元素
 * @param tree 指向二叉樹結構體的指標
 * @param value 指向要查詢內容的指標
 * @return 是否包含要查詢的值
 */
bool treeset_contain(const treeset* tree, const void* value);

  這部分函式實現如下:

  下面這個函式用於初始化二叉樹,其中引數elemsize表示每個樹節點要存放的值所佔記憶體大小,例如每個節點要儲存一個整數,則elemsize的值應該為sizeof(int),這個大小將直接反映在每個樹節點上,由節點結構體分量data來表示。comp引數是一個函式指標,前面定義過,用來表示比較兩個值的大小。

/**
 * 初始化二叉樹結構體
 * @param tree 指向二叉樹結構體的指標
 * @param elemsize 每個元素的大小
 * @param comp 用於比較的函式指標
 */
void treeset_init(treeset* tree, size_t elemsize, treeset_compare_t comp)
{
	tree->root = NULL;
	tree->count = 0;
	// 設定節點儲存的元素大小
	tree->elemsize = elemsize;
	// 設定用於元素大小比較的函式指標
	tree->compare = comp;
}
  下面這個函式用於釋放二叉樹所佔據的記憶體空間,其中呼叫了一個remove_all_node函式,後面介紹
/**
 * 釋放二叉樹佔據的空間
 * @param tree 指向二叉樹結構體的指標
 */
 void treeset_free(treeset* tree)
 {
 	// 從頭節點開始移除二叉樹中所有的節點
	remove_all_node(tree->root);
	// 將二叉樹所有內容還原為空
	memset(tree, 0, sizeof(*tree));
 }
  下面這個函式用於向二叉樹中存放內容,存放的原則就是從二叉樹頭節點開始,依次進行比較,比節點值大的放一邊,小的放另一邊,相當於二分查詢和插入,具體方式如圖:


/**
 * 向二叉樹中新增元素
 * @param tree 指向二叉樹結構體的指標
 * @param value 指向要新增元素的指標
 * @return 是否實際添加了節點
 */
int treeset_add(treeset* tree, const void* value)
{
	int result = 0;

	// 判斷二叉樹中是否有頭節點
	if (tree->count == 0)
	{
		// 建立頭節點
		tree->root = create_new_node(tree, value);
		result = 1;
	}
	else
	{
		/*
			對於新增節點,具體操作如下:
			1. 通過要新增節點的值和現有某個節點(從頭節點開始)進行比較(通過指定的比較函式進行)
			2. 如果要新增的節點值和現有某個節點值相同,則無需新增節點
			3. 如果要新增的節點值和現有某個節點值不同,則根據比較結果繼續訪問該節點的左支或者右支
			新增流程示意圖參看[圖1]
		 */
		// 表示比較結果
		int comp;
		// node變數指向要比較的節點,從頭結點開始;parent變數指向其父節點
		btree_node* node = tree->root, *parent;

		// 遍歷所有節點,直到沒有節點為止
		while (node)
		{
			// 儲存父節點指標
			parent = node;
			// 比較要新增的值和當前節點值
			comp = tree->compare(value, node + 1);
			// 判斷比較結果
			if (comp == 0)
				break;	// 節點值與要新增的值相同,停止流程
			if (comp > 0)
				node = node->rchild;	// 要新增的值大於節點值,繼續訪問當前節點的右支
			else
				node = node->lchild;	// 要新增的值小於節點值,繼續訪問當前節點的左支
		}
		// 如果迴圈結束且比較結果不為0,表示整個樹中沒有和要新增節點值相同的節點,需要通過新增新節點來儲存改值
		if (comp != 0)
		{
			// 建立新的節點並儲存節點值
			node = create_new_node(tree, value);
			// 為新節點設定父節點,為遍歷結束時最後一個有效節點
			node->parent = parent;
			// 根據比較結果設定新節點的位置
			if (comp > 0)
				parent->rchild = node;	// 新節點值大於最後一個樹節點值,新增為該樹節點的右孩子
			else
				parent->lchild = node;	// 新節點值小於最後一個樹節點值,新增為該樹節點的左孩子
			result = 1;
		}
	}
	// 修改節點數
	tree->count++;
	// 返回已新增的節點
	return result;
}

  下面這個函式用於從二叉樹中刪除一個節點,刪除的步驟較為複雜,分為兩種情況,先讀懂圖例,程式碼就很好理解了。程式碼中的find_node函式後面介紹:


/**
 * 從二叉樹中刪除一個元素
 * @param tree 指向二叉樹結構體的指標
 * @param value 要刪除的節點內容
 * @return 是否刪除了節點
 */
int treeset_remove(treeset* tree, const void* value)
{
	// 根據要刪除的節點值查詢要刪除的節點
	btree_node* node = find_node(tree, value);

	// 判斷是否找到要刪除的節點
	if (node)
	{
		/*
			對於刪除節點,具體操作如下:
			1. 判斷要刪除的節點情況:(1)是否同時具備左右支 (2) 是否只具備左支或右支
			2. 對於情況(1),需要將要刪除節點的值和該節點右孩子的左支最末節點值進行交換(參加圖2),確保交換後二叉樹仍保持正確結構,將問題轉為情況(2)
			3. 對於情況(2),只需要將要刪除節點的父節點和要刪除節點的子節點(左支或右支)建立關係,讓要刪除節點脫離樹結構即可
			4. 對於要刪除節點沒有子節點的情況,只需要讓被刪除節點的父節點失去左孩子(或右孩子)即可
			新增流程示意圖參看[圖2]
		 */

		// 用於臨時儲存節點
		btree_node* temp;

		// 判斷要刪除的節點是否同時具有左支和右支
		if (node->rchild && node->lchild)
		{
			// 找到比要刪除節點值大的最小值,即節點右孩子的左支最末節點(也可以找必要刪除節點值小的最大值)
			temp = node->rchild;
			while (temp->lchild)
				temp = temp->lchild;

			// 將上一步找到節點的值複製到要刪除的節點中
			memcpy(node + 1, temp + 1, tree->elemsize);
			// 將要刪除節點指標重新指向前面找到的節點,此時要刪除的節點將不再同時具備左右支
			node = temp;
		}

		// 找到要刪除節點的左支或者右支
		temp = node->lchild ? node->lchild : node->rchild;
		// 判斷要刪除節點是否具備左支或者右支
		if (temp)
		{
			/*
			 * 建立要刪除節點父節點和要刪除節點左支(或右支)的聯絡,排除掉要刪除節點
			 */

			// 將被刪除節點孩子的父節點改為被刪除節點的父節點。即越過被刪除節點,建立被刪除節點上一代和下一代的直接聯絡
			temp->parent = node->parent;

			// 判斷要刪除的是否為頭節點
			if (node->parent)
			{
				// 判斷要刪除的節點是其父節點的左支或右支
				if (node == node->parent->lchild)
					node->parent->lchild = temp; // 若要刪除節點是其父節點的左孩子,則將其孩子節點設定為其父節點的左支
				else
					node->parent->rchild = temp; // 若要刪除節點是其父節點的右孩子,則將其孩子節點設定為其父節點的右支
			}
			else
				tree->root = temp;	// 將被刪除節點的孩子節點設定為頭節點
		}
		else
		{
			/*
			 * 如果要刪除的節點是一個葉節點(即沒有孩子的節點),則將該節點的父節點與該節點相關的左支或右支聯絡刪除即可
			 */

			// 判斷要刪除的節點是否為頭節點
			if (node->parent)
			{
				// 判斷要刪除的節點是其父節點的左支或右支
				if (node == node->parent->lchild)
					node->parent->lchild = NULL; // 若要刪除節點是其父節點的左孩子,則將其孩子節點設定空
				else
					node->parent->rchild = NULL; // 若要刪除節點是其父節點的右孩子,則將其孩子節點設定空
			}
			else
				tree->root = NULL;	// 將頭節點設定為空,此時表示最後一個節點被刪除,樹變為空樹
		}

		// 釋放節點所佔記憶體
		free(node);
		// 修改二叉樹節點總數
		tree->count--;
	}
	// 返回是否建立了新節點
	return node != NULL;
}

  下面的函式用於在樹中查詢一個節點,用到的find_node在後面介紹

/**
 * 在二叉樹中查詢一個元素
 * @param tree 指向二叉樹結構體的指標
 * @param value 指向要查詢內容的指標
 * @return 是否包含要查詢的值
 */
int treeset_contain(const treeset* tree, const void* value)
{
	// 返回是否能找到指定節點
	return find_node(tree, value) != NULL;
}

  程式碼中用到的幾個子函式如下:

  create_new_node用於建立一個節點,該節點可以容納btree_node結構體內容和額外的節點值內容:

/**
 * 建立一個新的樹節點
 * @param owner 節點所屬的樹結構體指標
 * @param value 要存放在結點中的內容指標
 * @return 返回樹節點指標
 */
static btree_node* create_new_node(treeset* owner, const void* value)
{
	// 分配節點記憶體,大小為節點大小加上要儲存元素值的大小
	btree_node* pn = (btree_node*)malloc(sizeof(btree_node) + owner->elemsize);
	// 設定節點分量值
	pn->lchild = pn->rchild = pn->parent = NULL;
	// 設定節點所屬的樹
	pn->owner = owner;
	// 將節點值複製到指定的節點中
	memcpy(pn + 1, value, sizeof(owner->elemsize));
	// 返回建立的節點
	return pn;  
}

  remove_all_node用於刪除某個節點及其子節點,如果傳入根節點,則刪除整棵樹
/**
 * 刪除所有的節點
 * @param node 節點指標
 * @note 該函式利用遞迴的方式對接點進行刪除
 */
static void remove_all_node(btree_node* node)
{
	if (node)
	{
		// 遞迴呼叫刪除指定節點的左支
		remove_all_node(node->lchild);
		// 遞迴呼叫刪除指定節點的右支
		remove_all_node(node->rchild);
		// 刪除當前節點
		free(node);
	}
}

  find_node用於查詢一個節點,依然是利用一邊小一邊大的原則來進行:
/**
 * 在二叉樹中查詢一個元素
 * @param tree 指向二叉樹結構體的指標
 * @param value 指向要查詢內容的指標
 * @return 找到的節點
 */
static btree_node* find_node(const treeset* tree, const void* value)
{
	// 先取得頭節點
	btree_node* node = tree->root;

	// 遍歷,直到無節點可訪問
	while (node)
	{
		// 利用比較函式比較節點儲存內容和待查詢內容
		int cmp = tree->compare(value, node + 1);
		if (cmp == 0)
			break;	// 查詢結束,已找到所需節點
		if (cmp > 0)
			node = node->rchild;	// 待查元素值比節點儲存值大,則進一步查詢節點的右支
		else
			node = node->lchild;	// 待查元素值比節點儲存值小,則進一步查詢節點的左支
	}
	// 返回查詢到的節點
	return node;
}


  好了,有了上述程式碼,樹就可以發揮作用了,現在重點講一下如何中序遍歷這棵樹且不用遞迴和棧(使用遞迴的方法很簡單,使用棧的方法網上也有一些,大家可以自行查詢),即使用迭代器的方法遍歷節點:

  首先,定義迭代器結構體,很簡單,只有一個節點指標存放當前訪問的節點:

/**
 * 定義迭代器
 */
typedef struct
{
	btree_node* cur;	// 當前迭代到的節點指標
} treeset_iterator;

  有了這個結構體,就可以實現如下幾個迭代器訪問函式:
/**
 * 針對二叉樹初始化迭代器
 * @param tree 指向二叉樹結構體的指標
 * @param iter 指向迭代器結構體變數的指標
 */
void treeset_iterator_init(const treeset* tree, treeset_iterator* iter);

/**
 * 檢視是否有下一個節點
 * @param iter 指向迭代器結構體變數的指標
 */
int treeset_iterator_hasmore(const treeset_iterator* iter);

/**
 * 令迭代器指向下一個位置
 * @param iter 指向迭代器結構體變數的指標
 * @param value 輸出一個值
 */
int treeset_iterator_next(treeset_iterator* iter, void* value);

  上述幾個函式實現如下:

  treeset_iterator_init用於初始化迭代器,令迭代器中的節點指標指向整棵樹中最左邊的節點。

/**
 * 針對二叉樹初始化迭代器
 * @param tree 指向二叉樹結構體的指標
 * @param iter 指向迭代器結構體變數的指標
 */
void treeset_iterator_init(const treeset* tree, treeset_iterator* iter)
{
	// 獲取二叉樹頭節點
	btree_node* node = tree->root;
	// 移動指標,指向整個二叉樹最左邊(值最小)的節點
	while (node->lchild)
		node = node->lchild;
	// 將找到的節點指標儲存在迭代器中
	iter->cur = node;
}

  treeset_iterator_hasmore函式用於判斷是否還能繼續訪問下一個節點
/**
 * 檢視是否有下一個節點
 * @param iter 指向迭代器結構體變數的指標
 */
int treeset_iterator_hasmore(const treeset_iterator* iter)
{
	// 返回迭代器是否還有下一個節點
	return iter->cur != NULL;
}

  treeset_iterator_next函式用於獲取當前節點的值並移動到下一個節點,移動的步驟較為複雜,請參看圖例。訪問的整個過程就是一個訪問和回朔的過程


/**
 * 令迭代器指向下一個位置
 * @param iter 指向迭代器結構體變數的指標
 * @param value 輸出一個值
 */
int treeset_iterator_next(treeset_iterator* iter, void* value)
{
	btree_node* node = iter->cur;
	// 判斷迭代是否結束
	if (!node)
		return 0;

	/*
		節點的迭代
		    對於一個二叉樹來說,總有一種方法可以依次訪問樹中的所有節點,但和線性結構不同,要遍歷樹中所有節點,必須按照一種規則和步驟:
		1. 在開始遍歷前,先用指標指向整個樹中最左邊的節點(即樹中值最小的節點),以此作為遍歷的起點;
		2. 每次總以當前節點右孩子的左支的最末節點作為迭代的下一個節點,該節點必然為比當前節點值大的最小值節點;
		3. 如果當前節點沒有右孩子,則訪問其父節點,並將不以當前節點為右孩子的父節點作為下一個節點
		4. 如果在第3步得到NULL值,表示整個遍歷結束
		遍歷流程參考[圖3]
	 */

	// 儲存節點值
	memcpy(value, node + 1, node->owner->elemsize);

	// 判斷當前節點是否有右孩子
	if (node->rchild)	// 有右孩子的情況
	{
		// 令指標指向當前節點的右孩子(如果該節點沒有左支,則該節點就作為迭代的下一個節點)
		node = node->rchild;
		// 通過迴圈令指標指向該節點左支的最末節點,該節點為迭代的下一個節點
		while (node->lchild)
			node = node->lchild;
	}
	else				// 沒有右孩子的情況
	{
		btree_node* temp;
		// 向上訪問當前節點的父節點
		do
		{
			temp = node;
			node = node->parent;
		} while (node && temp == node->rchild);	// 依次訪問當前節點的父節點,直到沒有父節點(到達頭節點)或者當前節點不是其父節點的右孩子
	}
	// 將當前迭代到的節點儲存在迭代器中
	iter->cur = node;
	return 1;
}

  好了,以上的程式碼即可完成所需的遍歷訪問,可以用如下程式碼進行測試:
/**
 * 用於比較兩個int值的函式
 * @param a 指向第一個int值的指標
 * @param b 指向第二個int值的指標
 * @return 0表示兩個值相同,正數表示a較大,負數表示b較大
 */
static int int_compare(const int* a, const int* b)
{
	return *a - *b;
}

/**
 * 中序遍歷顯示二叉樹內容
 * @param tree 指向二叉樹結構體的指標
 */
static void show_tree(const treeset* tree)
{
	// 定義分隔符
	const char* spliter = "";
	// 定義一個迭代器
	treeset_iterator iter;
	// 儲存值的變數
	int value;

	printf("    集合節點為:");

	// 初始化迭代器
	treeset_iterator_init(tree, &iter);
	// 遍歷直到訪問了所有的樹節點
	while (treeset_iterator_hasmore(&iter))
	{
		// 獲取當前節點,令迭代器指向下一個節點
		treeset_iterator_next(&iter, &value);
		// 輸出當前節點值
		printf("%s%d", spliter, value);
		spliter = ",";
	}
	printf("\n    元素總數%d\n", tree->count);
}

// 用於測試的數值
static int VALS[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};


int main()
{
	int n;
	// 定義一個樹結構
	treeset set;

#ifdef DEBUG
	// 在程式結束時顯示記憶體報告
	atexit(show_block);
#endif // DEBUG

	// 設定隨機數種子
	srand(time(0));

	// 利用隨機數打亂陣列內容
	for (n = 0; n < 1000; n++)
	{
		int a = rand() % (sizeof(VALS) / sizeof(VALS[0]));
		int b = rand() % (sizeof(VALS) / sizeof(VALS[0]));
		if (a != b)
		{
			VALS[a] ^= VALS[b];
			VALS[b] ^= VALS[a];
			VALS[a] ^= VALS[b];
		}
	}

	// 初始化樹結構
	treeset_init(&set, sizeof(int), (treeset_compare_t)int_compare);
	
	// 儲存元素
	printf("測試元素儲存\n");
	for (n = 0; n < sizeof(VALS) / sizeof(VALS[0]); n++)
		treeset_add(&set, &VALS[n]);
	printf("    二叉樹中存放了%d個元素\n", set.count);
	show_tree(&set);

	puts("");

	// 查詢元素
	printf("測試元素查詢:\n");
	for (n = 0; n < 10; n++)
	{
		int a = rand() % 50;
		if (treeset_contain(&set, &a))
			printf("    元素%d已存在\n", a);
		else
			printf("    元素%d不存在\n", a);
	}

	puts("");

	// 測試元素刪除
	printf("測試元素刪除\n");
	n = rand() % 20 + 1;
	printf("    刪除前元素%d%s\n", n, treeset_contain(&set, &n) ? "存在" : "不存在");
	treeset_remove(&set, &n);
	printf("    刪除後元素%d%s\n", n, treeset_contain(&set, &n) ? "存在" : "不存在");

	n = rand() % 20 + 1;
	printf("    刪除前元素%d%s\n", n, treeset_contain(&set, &n) ? "存在" : "不存在");
	treeset_remove(&set, &n);
	printf("    刪除後元素%d%s\n", n, treeset_contain(&set, &n) ? "存在" : "不存在");
	show_tree(&set);

	// 釋放樹結構
	treeset_free(&set);

	return 0;
}

  其中重點關注show_tree函式,該函式即利用迭代器完成了二叉樹的遍歷,且無需任何遞迴和棧的輔助,且迭代器的訪問可以隨時暫停或繼續,非常靈活方便。