1. 程式人生 > >《劍指Offer》題三十一~題四十

《劍指Offer》題三十一~題四十

教材 pri current erase num 操作 nullptr () 實現一個函數

三十一、棧的壓入、彈出序列

題目:輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否為該棧的彈出順序。假設壓入棧的數字均不相等。例如,序列{1, 2, 3, 4 ,5}是某棧的壓棧序列,序列{4, 5, 3, 2, 1}是該壓棧序列對應的一個彈出序列,但{4, 3, 5, 1, 2}就不可能是該壓棧序列的彈出序列。

分析:本題中的壓棧序列並非是一次全部壓入堆棧!如果沒有思路,可以舉一兩個例子,一步步分析壓棧、彈出的過程,從中找出規律。

三十二、從上到下打印二叉樹

題目一:不分行從上到下打印二叉樹。從上到下打印出二叉樹的每個節點,同一層的節點按照從左到右的順序打印。

分析:二叉樹的層次遍歷。

隊列解法:

void print_top_to_bottom(BinaryTreeNode *pRoot)
{
     if(pRoot == nullptr)  return; queue<BinaryTreeNode*> que;    // 用於層次遍歷 que.push(pRoot); while(!que.empty()) { BinaryTreeNode *pNode = que.front(); que.pop(); printf("%d\t", pNode->value); if(pNode->lChild != nullptr) que.push(pNode->lChild); if(pNode->rChild != nullptr) que.push(pNode->rChild); } }

小結:層次遍歷這一過程可概括為,首先把起始節點(如根節點)放入隊列,接下來每次從隊列的頭部取出一個節點,遍歷這個節點之後把它能到達的節點(如子節點)都依次放入隊列,重復這個遍歷過程,直到隊列中的節點全部被遍歷為止。

題目二:分行從上到下打印二叉樹。從上到下按層打印二叉樹,同一層的節點按從左到右的順序打印,每一層打印到一行。

分析:為了把二叉樹的每一行單獨打印到一行裏,我們需要兩個變量,toBePrinted變量表示在當前層中還沒有打印的節點數,nextLevel變量表示下一層的節點數。

解法:

void print_by_row(BinaryTreeNode *pRoot)
{
	if(pRoot == nullptr)	return;
	queue<BinaryTreeNode*> que;
	que.push(pRoot);
	int toBePrinted = 1;
	int nextLevel = 0;
	while(!que.empty()) {
		BinaryTreeNode *pNode = que.front();
		que.pop();
		toBePrinted--;
		printf("%d\t", pNode->value);
		if(toBePrinted == 0) {
			printf("\n");
			toBePrinted = nextLevel;
			nextLevel = 0;
		}
		if(pNode->lChild != nullptr) {
			que.push(pNode->lChild);
			nextLevel++;	
		}
		if(pNode->rChild != nullptr){
			que.push(pNode->rChild);	
			nextLevel++;
		}
	}
}

題目三:之字形打印二叉樹。請實現一個函數按照之字形順序打印二叉樹,即第一行按照從左到右的順序打印,第二層按照從右到左的順序打印,第三行再按照從左到右的順序打印,其他行以此類推。

分析:如果找不到解決辦法,可以試著用具體的例子一步步分析。可知,按之字形打印需要兩個棧,如果當前打印的是奇數層(第一層、第三層等),則先保存左子節點再保存右子節點到第一個棧裏;如果當前打印的是偶數層,則先保存右子節點再保存左子節點到第二個棧裏。之所以需要兩個棧,是因為棧是後入先出的容器,故用一個棧不能將同一層的節點依次打印出來。

解法:

void print(BinaryTreeNode *pRoot)
{
	if(pRoot == nullptr)	return;
	stack<BinaryTreeNode*> levels[2];
	int current = 0;
	int next = 1;
	levels[0].push(pRoot);
	while(!levels[0].empty() || !levels[1].empty()) {
		BinaryTreeNode *pNode = levels[current].top();
		levels[current].pop();
		printf("%d\t", pNode->value);
		if(current == 0) {
			if(pNode->lChild != nullptr)
				levels[next].push(pNode->lChild);
			if(pNode->rChild != nullptr)
				levels[next].push(pNode->rChild);
		}
		else {
			if(pNode->rChild != nullptr)
				levels[next].push(pNode->rChild);
			if(pNode->lChild != nullptr)
				levels[next].push(pNode->lChild);
		}
		if(levels[current].empty()) {
			printf("\n");
			current = 1 - current;
			next = 1 - next;
		}
	}
}

小結:本題表面看上去像層次遍歷,但不能用隊列來實現,故應該根據具體情況選用合適的數據結構和算法。

三十三、二叉搜索樹的後序遍歷序列

題目:輸入一個整數數組,判斷該數組是不是某二叉搜索樹的後序遍歷結果。假設輸入的數組的任意兩個數字互不相同。

分析:要完成這道題,需要先掌握後序遍歷這一定義。比如,對於後序遍歷序列{5, 7, 6, 9, 11, 10, 8},最後一個數字{8}是樹的根節點的值,而前面6個數字可以分為兩部分,第一部分{5, 7, 6}是左子樹節點的值,它們都比根節點的值小,第二部分{9, 11, 10}是右子樹節點的值,它們都比根節點的值大。接下來,我們可以用同樣的方法來處理這兩個子序列{5, 7, 6}和{9, 11, 10}。這其實就是一個遞歸的過程。

解法:

bool seq_of_BST(int *seq, int length)
{
	if(seq == nullptr || length <= 0)	return false;
	int root = seq[length - 1];
	// 在BST中,左子樹節點的值小於根節點的值
	int i = 0;
	for(; i < length - 1; ++i) {
		if(seq[i] > root)
			break;
	}
	// 在BST中,右子樹節點的值大於根節點的值
	for(int j = i; j < length - 1; ++j) {
		if(seq[j] < root)
			return false;
	}
	// 遞歸,判斷左子樹是不是二叉搜索樹
	bool left = true;
	if(i > 0)	left = seq_of_BST(seq, i);
	// 遞歸,判斷右子樹是不是二叉搜索樹
	bool right = true;
	if(i < length - 1)	right = seq_of_BST(seq + i, length - i - 1); 
	return (left && right);
}

三十四、二叉樹中和為某一值的路徑

題目:輸入一棵二叉樹和一個整數,打印出二叉樹中節點值的和為輸入整數的所有路徑。

提示:一般的數據結構教材中都沒有介紹樹的路徑,因此對大多數應聘者而言,這是一個新概念,此時我們可以試著從一兩個具體的例子入手,找到規律。

分析:路徑是從根節點出發到葉節點,即路徑總是以根節點為起始點,因此我們首先需要遍歷根節點,故必須選擇前序遍歷這一方式。

三十五、復雜鏈表的復制

題目:請實現函數ComplexListNode* Clone(ComplexListNode *pHead),復制一個復雜鏈表。在復雜鏈表中,每個節點除了有一個m_pNext指針指向下一個節點,還有一個m_pSibling指針指向鏈表中的任意節點或者nullptr。

分析:本題中的復雜鏈表是一種不太常見的數據結構,並且復制這種鏈表的過程也較為復雜。我們可以把復雜鏈表的復制過程分解成3個步驟,這樣能幫我們理清思路。

三步解法:

ComplexListNode* Clone(ComplexListNode* pHead)
{
    CloneNodes(pHead);
    ConnectSiblingNodes(pHead);
    return ReconnectNodes(pHead);
}

void CloneNodes(ComplexListNode* pHead)
{
    ComplexListNode* pNode = pHead;
    while(pNode != nullptr)
    {
        ComplexListNode* pCloned = new ComplexListNode();
        pCloned->m_nValue = pNode->m_nValue;
        pCloned->m_pNext = pNode->m_pNext;
        pCloned->m_pSibling = nullptr;
 
        pNode->m_pNext = pCloned;
 
        pNode = pCloned->m_pNext;
    }
}

void ConnectSiblingNodes(ComplexListNode* pHead)
{
    ComplexListNode* pNode = pHead;
    while(pNode != nullptr)
    {
        ComplexListNode* pCloned = pNode->m_pNext;
        if(pNode->m_pSibling != nullptr)
        {
            pCloned->m_pSibling = pNode->m_pSibling->m_pNext;
        }
 
        pNode = pCloned->m_pNext;
    }
}

ComplexListNode* ReconnectNodes(ComplexListNode* pHead)
{
    ComplexListNode* pNode = pHead;
    ComplexListNode* pClonedHead = nullptr;
    ComplexListNode* pClonedNode = nullptr;
 
    if(pNode != nullptr)
    {
        pClonedHead = pClonedNode = pNode->m_pNext;
        pNode->m_pNext = pClonedNode->m_pNext;
        pNode = pNode->m_pNext;
    }
 
    while(pNode != nullptr)
    {
        pClonedNode->m_pNext = pNode->m_pNext;
        pClonedNode = pClonedNode->m_pNext;
 
        pNode->m_pNext = pClonedNode->m_pNext;
        pNode = pNode->m_pNext;
    }
 
    return pClonedHead;
}

  

三十六、二叉搜索樹與雙向鏈表

題目:輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。要求不能創建任何新的節點,只能調整樹中節點指針的指向。

三十七、序列化二叉樹

題目:請實現兩個函數,分別用來序列化和反序列化二叉樹。

三十八、字符串的排列

題目:輸入一個字符串,打印出該字符串中字符的所有排列。例如,輸入字符串"abc",則打印出由字符a、b、c所能排列出來的所有字符串abc、acb、bac、bca、cab和cba。

三十九、數組中出現次數超過一半的數字

題目:數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。

分析:最直觀的解決方法是將該數組排序,然後找出該數字,但這種方法的時間復雜度為O(nlogn)。我們要思考時間復雜度為O(n)的方法。

四十、最小的K個數

題目:輸入n個整數,找出其中最小的K個數。

提示:和上題一樣,我們可以將這n個整數排序,排序之後位於最前面的K個數就是最小的K個數,但這種思路的時間復雜度為O(nlogn)。我們需要更快的方法。

分析:我們可以先創建一個大小為K的數據容器來存儲最小的K個數字,接下來每次從輸入的n個整數中讀入一個數。如果容器中已有的數字少於K個,則直接把這次讀入的整數放入容器之中;如果容器中已有K個數字了,我們要做3件事情(①在K個整數中找到最大數;②有可能在這個容器中刪除最大數;③有可能要插入一個新的數字)。如果用一棵二叉樹來實現這個數據容器,比如最大堆,它每次可以在O(1)時間內得到已有的K個數字中的最大值,在O(logk)時間內完成刪除及插入操作。但我們自己從頭實現一個最大堆需要一定的代碼,在面試中很難完成,故我們還可以采用紅黑樹來實現這個容器,而在紅黑樹中的查找、刪除和插入操作都只需要O(logk)時間。因為set和multiset都是基於紅黑樹實現的,故本題可使用multiset來作為容器。

解法:

typedef multiset<int, greater<int>>			intSet;
typedef multiset<int, greater<int>>::iterator	setIterator;

void get_least_numbers(const vector<int> &data, intSet &leastNumbers, int k)
{
	if(k < 1 || data.size() < k)	return;
	leastNumbers.clear();
	vector<int>::const_iterator iter = data.begin();
	for(; iter != data.end(); ++iter) {
		if(leastNumbers.size() < k)
			leastNumbers.insert(*iter);
		// 容器已滿時 
		else {
			setIterator iterGreatest = leastNumbers.begin();
			// 在該multiset<int, greater<int>>類型中,第一個元素最大 
			if(*iter < *(leastNumbers.begin())) {	
				// 刪除最大數,插入一個新數	
				leastNumbers.erase(iterGreatest);
				leastNumbers.insert(*iter);
			}
		}
	}
}

小結:在上段代碼中,每次輸入一個新元素時,需要O(logk)時間,而總共有n個輸入數字,故總的時間效率就是O(nlogk)。  

  

  

《劍指Offer》題三十一~題四十