常見算法實現(筆記)

算法

牛客網編程題常見的編譯錯誤:

(1)常常有邏輯是對的,但是打印時沒有輸出結果的情況

原因:一般是輸入的測試數據有多組,但編寫的程序中沒有使用循環接收輸入數據,直接收了一組測試數據造成的;

(2)對於二叉樹等類似題型,提示堆棧溢出,遞歸或循環超出範圍的情況

原因:一般是首次進入樹序列時沒有判斷樹的根節點是否爲空;


算法描述問題:

答:描述算法的方法有多種,常用的有自然語言、結構化流程圖、僞代碼和PAD圖等,其中最普遍的是流程圖。
算法描述:

自然語言    也就是文字描述;

流程圖  特定的表示算法的圖形符號;

僞語言  包括程序設計語言的三大基本結構及自然語言的一種語言;

類語言  類似高級語言的語言,例如,類PASCAL、類C語言。

算法複雜度問題:

1)對數據進行壓縮存儲可以降低算法的空間複雜度;
2)算法的複雜度與問題的規模(即複雜度表達式的n)成正比
3)關於算法時間複雜度和空間複雜度:
答:對於三個複雜度符號:可以簡單的理解爲θ是一個區間,O是上限(也就是最壞情況),Ω爲下限(相當於最好情況),都是描述隨輸入量n的增長算法所花費的時間的增長情況。而一般情況下,我們都是使用o來表示複雜度(即最差的情況)。
時間複雜度計算方式:尋找算法中執行頻度最高的那個語句,算出其執行次數F(n),去掉F(n)的係數可得到f(n),那麼時間複雜度就是o(f(n)),其中n趨於無窮大。一般情況下,對於n趨於無窮大時,若頻次始終是常數的,那麼f(n)=1,所以時間複雜度爲o(1);若頻次是跟n爲線性關係,那麼f(n)=n,即時間複雜度爲o(n);其他以此類推。

空間複雜度計算方式:類似於時間複雜度計算。
空間複雜度包括:程序執行時所需要的存儲本身指令、常數、變量和輸入數據的空間,和一些對數據進行操作的工作單元、計算所需的輔助空間等。主要如下兩部分:
① 固定部分。這部分空間的大小與輸入/輸出的數據的個數多少、數值無關。主要包括指令空間(即代碼空間)、數據空間(常量、簡單變量)等所佔的空間。這部分屬於靜態空間。
② 可變空間,這部分空間的主要包括動態分配的空間,以及遞歸棧所需的空間等。這部分的空間大小與算法有關。
一個算法所需的存儲空間用f(n)表示。S(n)=O(f(n))  其中n爲問題的規模,S(n)表示空間複雜度。

1、各種排序算法

答:常用的幾種排序算法可參考:https://zhuanlan.zhihu.com/p/27232454
內部排序:內排序是指待排序列完全存放在內存中所進行的排序過程,適合不太大的數量序列;
外部排序:指數據量過大,內存不能完全存儲而需要訪問外存數據的排序。

穩定排序:指相同的兩個元素在排序前後,其相對位置關係不變;
不穩定排序:排序前後,相同元素相對位置關係發生改變;
教科書上的八種排序算法,按穩定排序和不穩定排序分類:
穩定排序:插冒歸基
不穩定排序:快選堆希

其中8種排序方式的時間複雜度和空間複雜度如下表:

注:基數排序的時間複雜度一般也可表示爲O(r*n),當r較小時就近似爲O(n);

如上表所示:

(1)冒泡、選擇、直接插入排序統稱爲簡單排序,時間複雜度均爲O(n^2);

(2)希爾排序的時間複雜度與比較步長有關,一般可認爲O(n^1.3);

(3)選擇排序和堆排序的時間複雜度與初始序列排列順序無關;

(4)空間複雜度有3個不是o(1),分別是快、歸、基,其中快排又是相對較小的。

(5)比較排序的時間複雜度最多可以減少到O(nlogn),基數排序不是比較類排序,所以可以做到O(n);


(1)直接插入排序

答:①就是對一段數據序列,從第一個開始像摸撲克牌時那樣插入排序,如下圖:


所以其時間複雜度=O(n^2);
插入排序實現代碼參考:
void insertion_sort(vector<int> &v)  //插入排序算法        
{          
    int temp = 0;   
    for (int i = 1; i < v.size(); i++)          
    {          
        if (v[i - 1] > v[i])          
        {             
            temp = v[i];      
            for (int j=i-1; j > =0 && v[j] > temp; j--)         
            {           
                v[j+1] = v[j];          
            }     
            v[j+1] = temp;      
        }      
    }     
}  

②直接插入排序的優化:在查找插入位置時使用二分查找法,因爲前面都是有序序列,使用二分查找法速度更快;

(2)希爾排序

答:希爾(Shell)排序又稱爲縮小增量排序,它是一種插入排序。它是直接插入排序算法的一種威力加強版,即把原來的比較步長加大。排序如下例子:

如上圖,初始時,有一個大小爲 10 的無序序列。

第一趟排序中,我們不妨設 gap1 = N / 2 = 5,即相隔距離爲 5 的元素組成一組,可以分爲 5 組。

接下來,按照直接插入排序的方法對每個組進行排序。

第二趟排序中,我們把上次的 gap 縮小一半,即 gap2 = gap1 / 2 = 2 (取整數)。這樣每相隔距離爲 2 的元素組成一組,可以分爲 2 組。

按照直接插入排序的方法對每個組進行排序。

第三趟排序中,再次把 gap 縮小一半,即gap3 = gap2 / 2 = 1。 這樣相隔距離爲 1 的元素組成一組,即只有一組。

按照直接插入排序的方法對每個組進行排序。此時,排序已經結束

需要注意一下的是,圖中有兩個相等數值的元素 5 和 5 。我們可以清楚的看到,在排序過程中,兩個元素位置交換了

所以,希爾排序是不穩定的算法。

時間複雜度:希爾排序的時間複雜度跟比較步長的選擇有關,一般可以做到O(n^1.3);

代碼實現:

void ShellInsert(vector<int> &v, int step)//比較希爾排序代碼與簡單插入排序代碼的異同
{
	int temp=0;
	for(int i=step; i<v.size(); i++)
       {
		if(v[i]<v[i-step])
               {
			temp=v[i];
			for(int j=i-step; j>=0 && v[j]>temp; j=j-step)
                       {
				v[j+1]=v[j];
			}
			v[j+1]=temp;
		}
	}
}
void ShellSort(vector<int> v, vector<int> step)//最終排序
{
	//按增量數組step依次對序列做希爾排序
	for(int i=0; i<step.size(); i++)
	{
		ShellInsert(v, step[i]);
	}
}


(3)冒泡排序

答:①冒泡排序是數據前後相鄰數據比較,前比後大則兩者交換,之後繼續向後推進比較,一輪下來最大的數據會出現在序列最後。普通的冒泡排序時間複雜度始終是n^2。實現代碼略;

②針對冒泡排序的改進,即某一趟冒泡排序後沒有任何元素交換位置,則結束排序——設標誌位。改進部分如下:

void(vector<int> v)
{
    for(int i=0; i<v.size()-1;i++)
    {
    	bool flag=false;
	for(int j=0; j<v.size()-i;j++)
	{
	    if(v[j] > v[j+1])
	    {
		swap(v[j],v[j+1]);
		flag=true;//有交換就將標誌位置位
	    }
	}
	if(!flag)//若一趟結束都沒有一次交換,表示序列已經有序
	    break;
    }
}

(4)快速排序

答:確定一個基數,先從後向前尋找一個比基數小的數(若沒找到,就繼續找),交換。然後轉換比較方向,從前向後直到找到比基數大的,交換;再重複前面的過程。快排具體實現就是:從右找到第一個小於poviot(一般取序列中第一個數)的數,與之交換,同時left加1;然後再從左找到第一個大於poviot的數,與之交換,同時right減1;這樣循環往復,直到與poviot比較的左右數據的下標相同(left=right)爲止,此時poviot右邊的數都大於左邊,左邊的數都大於右邊。(注意:無論poviot被移到哪個位置,都是和它作比較)
例如:取key=49
49 38 65 97 76 13 27 原始數組 k=49
27 38 65 97 76 13 49 l=0,r=6(從後向前)
2738 65 97 76 13 49 l=1,r=6(從前向後,未找到)
27 38 49 97 76 13 65 l=2,r=6(從前向後)
27 38 13 97 76 49 65 l=2,r=5(從後向前)
27 38 13 49 76 97 65 l=3,r=5(從前向後)
27 38 13 49 76 97 65 l=3,r=4(從後向前,未找到)
27 38 13 49 76 97 65 l=3,r=3(從後向前,未找到)

注意:快速排序中最快速情況是:每一趟排序的基準值(一般第一個數據)都可以在一趟排序完成後,將當前序列平均分爲兩個個數相等的序列。最差的情況是,每次選取的基準數據都是當前序列中最小或最大值,即當前序列是有序序列,此時其會退化爲冒泡排序。簡單總結:快排相比其他排序算法最具優勢的情況是數值序列完全無序,最不具優勢的情況是數值序列基本有序;

代碼:

void quicksort(vector<int> &v,int left, int right)
{  
	if(left < right)//false則遞歸結束
	{    
		int key=v[left];//基數賦值
		int low = left;                
		int high = right;   
		while(low < high)	//當low=high時,表示一輪分割結束
		{                        
			while(low < high && v[high] >= key)//v[low]爲基數,從後向前與基數比較
			{                                
				high--;                        
			}
			swap(v[low],v[high]);

			while(low < high && v[low] <= key)//v[high]爲基數,從前向後與基數比較
			{                                
				low++;                        
			}      
			swap(v[low],v[high]);
		}                 
		//分割後,對每一分段重複上述操作
		quicksort(v,left,low-1);               
		quicksort(v,low+1,right);
	}
}

快排更多詳情參考:http://blog.csdn.net/xiongchao99/article/details/74524807#t3


(5)選擇排序

①每一次都遍歷數據序列,從待排序的數據元素中選出最小(或最大)的一個元素,順序放在被排序序列第一個位置,直到全部待排序的數據元素排完(第一輪:用第一個數與後面數據比較,後面小就與之交換位置,繼續用交換後的第一個和後續數據比較……,第二輪:用第二個數據和後面比較,類似第一輪方式,……,直到比較完所有數據)。 選擇排序是不穩定的排序方法。總的比較次數N=(n-1)+(n-2)+...+1=n*(n-1)/2,與數據的初始排列順序無關。實現代碼略。

②除此之外還有樹形選擇排序,即錦標賽排序。如一個數據序列中找出最大的和第二大的數,用競標賽思想解決最好:如有序列ABCDEFGH共8個數據的序列,找出最大和第二大的兩個,需要比較的次數,見下圖:


由圖可知,找最大值用了7次比較,第二大值用了2次比較,共計用9次比較。

實現代碼略;

(6)堆排序

答:(1)堆排序是樹形選擇排序的改進型,其避免了後者較大的空間複雜度;

注意:使用的最小/最大堆都是完全二叉樹;

首先可以看到堆建好之後堆中第0個數據是堆中最小的數據。取出這個數據,再根據章節二中數據結構的堆刪除方式,執行下堆的刪除操作並進行堆恢復工作。這樣堆中第0個數據又是堆中最小的數據,重複上述步驟直至堆中只有一個數據時就直接取出這個數據。
       由於堆也是用數組模擬的,故堆化數組後,第一次將A[0]與A[n - 1]交換,再對A[0…n-2]重新恢復堆。第二次將A[0]與A[n – 2]交換,再對A[0…n - 3]重新恢復堆,重複這樣的操作直到A[0]與A[1]交換。由於每次都是將最小的數據併入到後面排好序的數據序列前面,故操作完成後整個數組就有序了。注意使用最小堆排序後是遞減數組,反之,最大堆排序後是遞增數組
堆頂刪除後的排序也可見下例:


根據堆的刪除規則,刪除操作只能在堆頂進行,也就是刪除0元素。
然後讓最後一個節點放在堆頂,做向下調整工作,讓剩下的數組依然滿足最小堆。
刪除0後用8填充0的位置,爲[8,3,2,5,7,4,6]
然後8和其子節點3,2比較,結果2最小,將2和8交換,爲:[2,3,8,5,7,4,6]
然後8的下標爲2,其兩個孩子節點下標分別爲2*2+1=5,2*2+2=6
也就是4和6兩個元素,經比較,4最小,將8與4交換,爲[2,3,4,5,7,8,6]
這時候8已經沒有孩子節點了,調整完成。
每次堆刪除後的堆恢復時間複雜度爲O(logn),堆排序總時間複雜度=O(nlogn);空間複雜度=O(1);
代碼實現(以最大堆爲例,排序後是遞增序列):
//注意:爲了計算方便,默認數組從下標1開始,即數組v的首元素空着;
void HeapAdjust(vector<int> v, int start, int end)//start是待調整堆的堆頂元素下標,end即爲待調整堆的最後一個元素下標
{
	int top=v[start];
	for(int j=2*start; j<=end; j=2*j)
	{
		if(j<end && v[j]<v[j+1])
			j++;
		if(top<v[j])
		{
			v[start]=v[j];
			start=j;
		}
		else
			break;
	}
	v[start]=top;
}

void HeapSort(vector<int> &v)	//最終排序
{
	//建堆(從下往上進行子堆調整即可實現)
	for(int i=v.size()/2; i>0; i--)	//i=v.size()/2是倒數第二層的最後一個非葉子節點
		HeapAdjust(v, i, v.size()); //本來end應該是當前子堆的最後一個元素,但使用整堆的最後一個元素下標依然可以;

	//取堆頂元素與堆末尾交換並調整剩下的前半部分堆元素
	for(int j=v.size(); j>=1; j--)
	{
		swap(v[1], v[j]);
		HeapAdjust(v, 1, j-1);
	}
}


(2)除此之外,C++的STL中也有建堆和堆調整的函數可調用,有make_heap()。用STL函數實現堆排序具體方式如下:

①代碼簡寫:

vector<int> v;
make_heap(v.begin(),v.end());//建堆
for(;;){
	……//交換堆頂和堆尾
	make_heap(v.begin(),v.end()-i);//堆調整(用建堆函數實現)
}

②用pop_heap()/push_heap()可以實現堆頂刪除調整和堆尾插入調整:

vector<int> v;
pop_heap(v.begin(),v.end());//先pop_heap,然後在容器中刪除
v.pop_back();
v.push_back(temp);//先在容器中加入,再push_heap
push_heap(v.begin(),v.end());
堆的刪除和插入的語句順序必須如上述一樣:

刪除堆頂pop_heap實際並沒有刪除,只是將堆頂元素放到堆尾,然後對前面剩下的對元素進行堆調整。要實實在在的刪除後面就還需要調用pop_back();

插入元素push_heap則要注意必須在push_back語句操作後面,否則無法實現堆調整。

(3)top K問題一般可以使用的算法有堆排序、快排、選擇排序。

其中,堆排序用的比較多,因爲對於大量數據,可實現NlogK的實現複雜度。其中,具體的可以逐個元素的對堆進行刪除/插入,遍歷完所有元素後得到的堆元素就是TOP K。

注意:最大的K個元素用最小堆(小根堆),相反最小的K個元素用最大堆(大根堆);

這裏給出兩種實現最大K個元素的代碼:①make_heap調整堆:

vector<int> GetLeastNumbers_Solution(vector<int> input, int k){
	int len=input.size();
	if(len<=0||k<=0||k>len) 
            return vector<int>();
	vector<int> v(input.begin(),input.begin()+k);//用測試數組前k個元素初始化堆數組
	make_heap(v.begin(),v.end());//默認最大堆,要建立最小堆可以添加第三個參數greater<int>()
	for(int i=k;i<input.size();i++){
		if(input[i]>v[0]){	//逐個替換堆頂並調整	
			v.push_back(input[i]);
			swap(v[0],v[k]);
			v.pop_back();
			make_heap(v.begin(),v.end());
		}
	}
	return v;
}
②刪除/插入調整堆(注意pop_heap和pop_back以及push_heap和push_back的順序):
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
    int len=input.size();
    if(len<=0||k<=0||k>len) 
        return vector<int>();
	vector<int> v(input.begin(),input.begin()+k);
	make_heap(v.begin(),v.end());
	for(int i=k;i<input.size();i++){
		if(input[i]<v[0]){			
			pop_heap(v.begin(),v.end());//先pop_heap,然後在容器中刪除
			v.pop_back();
			v.push_back(input[i]);//先在容器中加入,再push_heap
			push_heap(v.begin(),v.end());
		}
	}
	sort(v.begin(),v.end());
	return v;
}

(7)歸併排序

答:n個元素k路歸併排序的歸併趟數:s=logk(n);常見的歸併排序是2路歸併排序,其時間複雜度=log(n),空間複雜度=O(n);
排序過程:步驟一,先將序列中的數據從中間平分爲兩組,然後每組再平分爲兩組,……,以此類推,直到最後每組只有一個元素爲止;步驟二,兩兩相鄰組進行歸併排序成一組,再將歸併後的緊鄰兩組作爲一個新的組合進行歸併排序,依次類推,直至序列變爲一個組爲止。
代碼實現:
//歸併兩個子序列
void Merge(vector<int> v1, vector<int> &v2, int start, int mid, int end)	 //用一個序列裝兩組數組,使用start、mid、end區分不同序列
{
     //將分組v1[start,...., mid]與分組v1[mid+1, ... ,end]歸併爲一個數組序列
     int i=start, j=mid+1,k=0;
     while(i<= mid && j<=end )
     {
	   if(v1[i]<v1[j])
		v2[k++]=v1[i++];
	   else
		v2[k++]=v1[j++];
     }
     if(i<=mid)
	  v2[k, ... ,end]=v1[i, ... ,mid];	//簡要表示
     else if(j<=end)
	  v2[k, ... ,end]=v1[j, ... ,end];	//簡要表示
     //將排好序的新序列v2覆蓋到原序列v1中
     v1[start, ..., end]=v2[0, ..., v2.size()-1];  //簡要表示
}
//最終排序(先分割再歸併,遞歸實現)
vector<int> v2;	  //創建輔助數組(最終長度爲n)
void MergeSort(vector<int> &arr, int start,int end)
{
     if(start<end)
     {
           int mid=(start+end)/2;
           MergeSort(arr, start, mid);   //遞歸生成左子節點(分割出左序列)
 	   MergeSort(arr, mid+1, end);   //遞歸生成右子節點(分割出右序列)
	   Merge(arr, v2, start, mid, end);   //對樹當前層上述兩個序列進行歸併
     }
}
兩個函數合在一起的具體實現代碼如下:
void mergeSort(vector<int>& data, int start, int end) {
	// 遞歸終止條件
	if(start >= end) {
		return 0;
	}
	// 遞歸
	int mid = (start + end) / 2;
	mergeSort(data, start, mid);
	mergeSort(data, mid+1, end);

	// 歸併排序,並計算本次逆序對數
	vector<int> copy(data); // 數組副本,用於歸併排序
	int foreIdx = mid;// 前半部分的指標
	int backIdx = end;// 後半部分的指標
	int counts = 0;// 記錄本次逆序對數
	int idxCopy = end;// 輔助數組的下標
	while(foreIdx>=start && backIdx >= mid+1) {
		if(data[foreIdx] > data[backIdx])
			copy[idxCopy--] = data[foreIdx--];
		else 
			copy[idxCopy--] = data[backIdx--];
		
	}
	while(foreIdx >= start) {
		copy[idxCopy--] = data[foreIdx--];
	}
	while(backIdx >= mid+1) {
		copy[idxCopy--] = data[backIdx--];
	}
	for(int i=start; i<=end; i++) {
		data[i] = copy[i];
	}
}
在歸併排序中,本意是歸併後的輔助空間v2作爲下一個歸併的源序列,然後再開闢加倍大小的空間作爲新的輔助空間,這樣實際的空間複雜度會是O(nlogn);爲了避免空間複雜度過大,本例採用如下方式:
只在開始開闢了一個長度爲n的數組v2作爲輔助空間,後面的歸併都是將排好序的子序列覆蓋到原始數組相應位置,然後仍然以原始數組作爲歸併的源序列,這樣輔助空間v2就還可以作爲輔助空間使用。所以可以做到空間複雜度=O(n);

上述排序中的第二個函數使用遞歸實現序列分割與歸併,原理可見下圖:

或見下圖:



--------------------------上述7個排序是比較排序,其時間複雜度的極限是O(nlogn),下面將介紹非比較排序,時間複雜度可達到O(n)---------------------------
計數排序——時間複雜度O(k+n),要求:被排序的數是0~k範圍內的整數。一般情況下k較小,所以時間複雜度=O(n);
基數排序——時間複雜度O(d(k+n)),要求:d位數,每個數位有k個取值。一般情況下k和d較小,所以時間複雜度=O(n);
桶排序——時間複雜度O(n),要求:被排序數在某個範圍內,範圍過大的話桶的個數會過多;

總結:非比較排序的時間複雜度都可以達到O(n),但其空間複雜度都比較高,就是所謂的用空間換時間;

(8)基數排序

答:多關鍵字排序,舉例說明:
①書上舉例是撲克牌排序:四種花色“梅花<方塊<黑桃<紅桃”,紙牌面值“2<3<.....<K<A”。這樣每個紙牌就有兩個關鍵字;先按面值分爲13堆,再將這些堆按順序疊加在一起,最後按花色從前面撲克牌中按順序抽取出來存放即可;
②本例以常見的3位數以下的整數排序爲例:每個數據的個、十、白位分別作爲一個關鍵字,那麼每個數據有三個關鍵字。
基數排序(radix sort)屬於「分配式排序」,有點類似 「桶排序」,排序方式如下:
1°、分配10個桶,桶編號爲0-9,以個位數數字爲關鍵字依次入桶,將桶裏的數字順序取出來;
2°、再次入桶,不過這次以十位數的數字爲關鍵字,進入相應的桶,同一桶內有序;
3°、再次按順序取出,排序完成;

注意:比較順序必須是個、十、百,否則無法比較出結果;數據序列最大爲4位、5位或更多位數的以此類推。
代碼實現:
此處不寫具體代碼,但注意“10個桶可以直接用二維數組表示,如上序列有5個數據,桶就可以爲v[10][5]”。同時,每個桶都可以重複使用後,只需要在下一輪按順序讀出數據後將桶復位爲空即可;
時間複雜度=O(n),空間複雜度=O(r*n+d*n),其中r是關鍵字取值個數,d是關鍵字種類數;

補充:鏈式基數排序——將待排序序列用鏈表存儲,解決原基數排序空間複雜度較高的困境,可以做到空間複雜度爲常數(前提是原序列是用鏈表存儲)。


補充兩個排序:計數排序與桶排序,兩者是在基數排序前提出的,也是上述基數排序的基礎;
(9)計數排序
計數排序基本思想:輸入一個數X,確定小於X的元素的個數,這樣,就可以把這個數放在輸出數組的指定位置上。假設輸入數組是A[n],則需要一個輔助數組C[0...k],一個輸出數組B[n]。其中,k代表輸入數組中的最大值,n代表輸入數組的長度。輸入數組A是待排序排序的數據,輸出數組B是需要排序完成後的數據,輔助數組中是按鍵值存儲該鍵值在輸入數組中出現的次數。
優點:時間複雜度=O(n),對於最大元素的值較小時,使用計數排序是極快的;
缺點:空間複雜度較高,若排序數組最大值爲1000,就需要一個長度爲1001的數組作爲輔助數組。所以,計數排序不適合數值較大的情況。

(10)桶排序(bucket sort)
答:計數排序是假設輸入的數據都屬於一個小區間內的整數,而桶排序則假設輸入是由一個隨機過程產生的。該過程將元素均勻、獨立的分佈在區間[0,1)上。桶排序的過程可如下:
①將[0,1)平分爲10(其他數字也可)個區間,即創建10個桶,然後遍歷序列,將相應數據添加到各個桶中;
②使用其他比較排序方式如快排對每個桶排序;
③按桶的順序和桶中數據存放順序依次讀出所有數據,排序完成。

2、KMP算法

答:(1)KMP算法是字符串匹配算法,它由簡單字符串匹配(BF)轉化而來。其中,若串長爲n,模式串長爲m,則
BF算法(普通匹配算法):時間複雜度O(m*n);空間複雜度O(1);
KMP算法:時間複雜度O(m+n);空間複雜度O(n);
KMP算法需要模式函數值數組next[m],用於輔助。

例如,在S=”abcabcabdabba”中查找T=”abcabd”,如果使用KMP匹配算法,當第一次搜索到S[5] T[5]不等後,S下標不是回溯到1T下標也不是回溯到開始,而是根據TT[5]==’d’的模式函數值(next[5]=2,爲什麼?後面講),直接比較S[5] T[2]是否相等,因爲相等,ST的下標同時增加;因爲又相等,ST的下標又同時增加,最終在S中找到了T。如圖:

所以,KMP算法相比普通匹配算法最大的優點就是:主字符串S的指針不需要回溯。

(2)next數組求取方法:
KMP的next數組求解就是求解模式串相同前綴後綴的過程:
①取i=0元素以前的子串,求解其前綴後綴相同的個數,記爲nx[0];
取i=1元素以前的子串,求解其前綴後綴相同的個數,記爲nx[1];
以此類推,……
③將nx數組每個元素分別右移一位,數組首元素賦值-1,得到的新數組就是next(注:得到的next是對應模式串下標從0開始的情況);

3、折半查找法(二分查找法)

答:要求:

1.必須採用順序存儲結構

2.必須按關鍵字大小有序排列(不一定要升序)。

方法:取正中間進行比較,小則丟掉正中間被比較數大的一側所有數據,繼續採用二分查找比較小的一側數據。查找中,偶數個數據取正中間靠近起始方向的數據比較,奇數個數據取正中間的。

時間複雜度:o(lgN)


4、螞蟻爬行算法

答:n只螞蟻以每秒1cm的速度在長爲Lcm的竹竿上爬行。當螞蟻看到竿子的端點時就會落下來。由於竿子太細,兩隻螞蟻相遇時,它們不能交錯通過,只能各自反方向爬行。對於每隻螞蟻,我們只知道它離竿子最左端的距離爲xi,但不知道它當前的朝向。請計算所有螞蟻落下竿子的最短時間和最長時間。
問題的要點:螞蟻相遇後反方向爬行當做穿透對方繼續爬行。故最大時間就是離某一端點最遠的螞蟻用時,最小時間則爲離某端點最近的螞蟻用時中的最大者。


5、漢諾塔(Hanoi塔)

問題:漢諾塔問題中有三根杆子A,B,C。A杆上有N個(N>1)穿孔圓盤,盤的尺寸由下到上依次變小。要求按下列規則將所有圓盤移至C杆:

①每次只能移動一個圓盤;

②每個杆上大盤不能疊在小盤上面;

③根據A上原有圓盤個數k,要完成A上所有圓盤移出至C需要移動次數爲。

漢諾塔移動次數的公式:f(k+1)=2*f(k)+1;其中,f⑴=1,f⑵=3,f⑶=7)

爲什麼呢?假設有4個圓盤,那麼移動過程可以如下描述:


①中其中將1,2,3號移動到B,移動次數爲f(3);

②中只是將最底下的圓盤移動到空杆C上,那麼移動次數就是1;

③中將A作爲輔助,移動B上的1,2,3號到C上,移動次數同①,仍然爲f(3);

故總次數爲:f(4)=2*f(3)+1;

以上移動流程具有普適性,可以推廣到k=n,故可得公式:f(k+1)=2*f(k)+1


6、常見的電梯調度算法

答:電梯調度算法:

1)電梯有移動方向,各樓層的請求有請求方向,這裏維護一個請求表(記錄請求ID,請求方向,該請求的停靠樓層);

2)電梯按照一個方向移動,直到該方向沒有請求,不會根據某一層的請求方向突然改變電梯的移動方向。但是注意:電梯在移動過程中只處理與“電梯移動方向”相同請求方向的請求。如電梯向下移動,只處理電梯下方樓層的請求,且該請求的方向也向下(停靠樓層請求無方向)。若請求樓層在向下方向,但請求方向不是向下,是不做處理的;

3)沒完成一個請求,就從請求表中刪除該請求記錄;

4)若移動方向上已經沒有請求(這個請求不僅包括請求表中的請求樓層,還包括停靠樓層),但電梯移動方向的反方向有請求,就把電梯移動方向置位爲反方向;

實際上,電梯調度算法和一些操作系統調度算法如磁盤尋道是類似的。

請看下面例子:









7、動態規劃(DP)問題

答:動態規劃算法通常基於一個遞推公式及一個或多個初始狀態。當前子問題的解將由上一次子問題的解推出。使用動態規劃來解題只需要多項式時間複雜度,因此它比回溯法、暴力法等要快許多。
基本原理:首先,要找到某個狀態的最優解,然後在它的幫助下,找到下一個狀態的最優解
基本思路:我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以後是否被用到,只要它被計算過,就將其結果填入表(二維數組)中。這就是動態規劃法的基本思路。具體的動態規劃算法多種多樣,但它們具有相同的填表格式。

經典例子解釋:
(1)01揹包問題
題目:一個旅行者準備隨身攜帶一個揹包,可以放入揹包的物品有n種,每種物品的重量和價值分別爲wj, vj。 如果揹包的最大重量限制是b,怎樣選擇放入揹包的物品以使得揹包的價值最大?

目標:
約束條件:
遞推公式和邊界約束方程:


上述公式用C++代碼表示:
i是物件編號,j是揹包允許的重量,dp是最大價值數組;
dp[i][0]={0};
dp[0][j]={0};
dp[i][j] = Max(dp[i - 1][ j ],dp[ i - 1 ][  j- weight[ i ] ]+ value[ i ]);//注意,此語句前要添加if(j>=weght[i])判斷語句。

上述所有的元素都存儲在dp數組中,後續需要只需直接取出即可;

該算法的代碼實現就是通過對dp[][]二維數組進行操作實現的,具體就是使用兩個for的嵌套循環,實現代碼(C++)如下:
for(int i=1;i<=N;i++)
{
        for(int j=1;j<=M;j++)
        {
                 if(v[i]<=j)
                 {
                        dp[i][j] = dp[i-1][j-v[i]]+v[i]>dp[i-1][j] ? dp[i-1][j-v[i]]+v[i]:dp[i-1][j];
                 }
                 else
                        dp[i][j]=dp[i-1][j];
         }
}
其中,dp[][]數組存儲的就是價值,i是物件編號,j是允許的最大重量,v是單件價值;

01揹包的內存優化:
由dp[i][j] = Max(dp[i - 1][ j ],dp[ i - 1 ][  j- weight[ i ] ]+ value[ i ])可知,dp[i][j]的計算只和dp[i-1]相關,即沒有使用其他子問題,因此在存儲子問題的解時,只存儲dp[i-1]子問題的解即可,這樣可以用兩個一維數組解決,一個存儲子問題,一個存儲正在解決的子問題,如predp[ ]存儲子問題,dp[ ]存儲當前的。進一步思考,由於我們可以使得j由大到小遞減變化(與上一個未優化內存的不同),而計算dp[i][j]時又只使用了dp[i-1][0……j],沒有使用dp[i-1][j+1],即j值指向一個方向變化,這樣的話,我們先計算j的循環時,讓j=M……1,只使用一個一維數組即可。
代碼如下:
for (int i=1; i<=N; i++)  
        for (int j=M; j>=1; j--)  
        {  
            if (weight[i]<=j)  
            {  
                f[j]=max(f[j],f[j-weight[i]]+value[i]);  //被修改的f[j]這一輪循環後續部分就不會再用了,所以直接用一個數組即可
            }             
        }  

(2)完全揹包問題
完全揹包相對於01揹包的區別是每種物品不止一件,可能有無限件,這樣揹包問題就可以用下面狀態轉移方程:
f[i][j]=Max( f[i-1][j],f[i-1][ j - k*weight[i] ] + k*value[i]),其中0<=k<=V/weight[i+1];
即將原來選或者不選第i種物品改爲選0、1、……、k件第i種物品;
該算法的代碼實現就是通過對dp[][]二維數組進行操作實現的,具體就是使用三個for的嵌套循環,實現代碼(C++):
for (int i=1; i<=N; i++)  
        for (int j=1; j<=M; j++)  
        {  
            for(int k=1;k<K;k++)
            {
                   if (k*weight[i]<=j)  
                  {  
                        f[i][j]=max(f[i-1][j],f[i-1][j-k*weight[i]]+k*value[i]);  
                  }     
                  else
                       break;   
              }      
        }  
}
完全揹包內存優化:
1)直接篩選法:完全揹包問題有一個很簡單有效的優化,是這樣的:若兩件物品i、j滿足v[i]<=v[j]且w[i]>=w[j],則將物品j去掉,不用考慮,代碼略;
2)轉化爲01揹包問題:將同種物品中每件物品提升爲每種物品;
舉例:物品個數N = 3,揹包容量爲V = 5。
拆分之前的物品序列:

拆分之後的物品序列:

根據上述思想:在揹包的最大容量(5)中,最多可以裝入1件物品一,因此不用擴展物品一。最多可以裝入2件物品二,因此可以擴展一件物品二。同理,可以擴展一件物品三。

最後,對拆分拓展後的物品序列進行01揹包計算即可。

代碼實現如下(與01優化揹包相同):
for (int i=1; i<=N; i++)  
        for (int j=M; j>=1; j--)  
        {  
            if (weight[i]<=j)  
            {  
                f[j]=max(f[j],f[j-weight[i]]+value[i]);  
            }             
        }  


8、英語句子按單詞爲單位逆序

答:一般逆序操作都可以用棧實現;
例如C++中:
定義string類型的棧:stack<string> st;
以及3個常用函數:
string str;
push(str); //將字符串壓棧
pop(str); //將字符串彈出棧
top(); //返回棧頂元素的引用(注意,此處返回的是棧頂的字符串)

9、根據3條邊求三角形面積——海倫公式

答:如下,3邊長爲:m_a, m_b, m_c,海倫公式爲:
s=(m_a+m_b+m_c)/2;  
area=sqrt(s*(s-m_a)*(s-m_b)*(s-m_c));
實現主要代碼:
if((m_a+m_b>m_c) && (m_a+m_c>m_b) && (m_b+m_c>m_a))  //這個判斷一定需要有
{
         double s=(m_a+m_b+m_c)/2;   //算法主要部分
         double area=sqrt(s*(s-m_a)*(s-m_b)*(s-m_c));
} 

10、迴文序列相關問題

答:迴文序列是指字符串正着讀和反着讀都是一樣的。
(1)字符串是否迴文串的判斷:
使用棧進行操作——若字符串序列長度爲偶數,則將串的前一半依次壓入棧中,在依次彈出並與剩下的一般字符串按順序依次比較,若對應都相同,則該字符串爲迴文序列。若串長爲奇數個,則直接舍掉中間那個,其他按照偶數個操作即可;

(2)字符串最大回文子串查找:
注意區分:最大回文子序列和最大回文串,前者相鄰元素在原串中可以不連續(只保證順序一致即可),後者在原串中必須是連續的;
1)蠻力法:
時間複雜度爲o(n^3)——先將字符串的所有子串求出來,然後對每個子串判斷是否爲迴文串(子串判斷是否爲迴文串使用(1)中方式);
大致寫一下過程:
cin>>s;   //輸入字符串
int  maxlen=0;
int n=s.size();
string str;
int count=0;
int p,q=0;

for(int i=0;i<n;i++){
      for(int j=i;j<n;j++){
             str=s.substr(i,j-i+1);

             p=0;
             q=str.size()-1;    //截取子串
             while(p<q){              //判斷子串是否迴文串
                   if(str[p]!=str[q]){
         break;
                   } 
                   p++;q--;              
             }
             if(maxlen<str.size())
                   maxlen=str.size();
             count++;
      }
}

2)蠻力法改進:
說明:迴文串必定是對稱的,所以可以一一遍歷每個元素,對各個元素進行左右對應數判斷是否相等,記錄每次遍歷得出的迴文長度;
過程:由於迴文對稱軸有兩種情況,①迴文長度爲奇數,對稱軸是正中間元素;②迴文長度爲偶數,對稱軸是正中間間隙;
爲了統一成奇數長度,我們對迴文可做改變如:ABCBA -> #A#B#C#B#A#,ABBA -> #A#B#B#A#,這樣偶數長度變爲了奇數長度,奇數長度的還是奇數長度,不用再考慮對稱軸在中間間隙的情況了;

時間複雜度o(n^2)——相對於蠻力法,其避免了一些重複的比較,減小了時間複雜度;
實現代碼如下:
int maxLen2(string str)
{
	string s;
	//添加輔助符#
	s.push_back('#');
	for(int k=0;k<str.size();k++){
		s.push_back(str[k]);
		s.push_back('#');
	}
	cout<<s<<endl;

	//依次把每個字符作爲對稱中心進行最長迴文判斷
	int len = s.size();   
	int maxlen=0;
    for (int i = 1; i < len-1; i++)	//s的首尾都是#,不屬於原字符串,可不計算
    {
		int count=1;
		while(i-count>=0&&i+count<=len-1 && s[i-count]==s[i+count]){
				count++;
		}
		if(maxlen<count-1)
			maxlen=count-1;
    }
    return maxlen;
}


3)manacher算法
第二種方法中改進算法的時間複雜度還是不夠好,可以繼續改進,就是manacher算法,可得到時間複雜度爲o(n);

算法的核心:用輔助數組p記錄以每個字符爲核心的最長迴文字符串半徑(也就是p[i]記錄了以str[i]爲中心的最長迴文字符串半徑。p[i]最小爲1,此時迴文字符串就是字符串本身),再結合“一個大回文串對稱軸的左右兩側分別有一個子串,且這兩個子串的位置相對大回文串對稱軸對稱,則其中一個子串是迴文,另一個子串一定也是迴文”的原理,免去重複計算;

算法核心圖示:
假設mx記錄了前期比較結果中擁有最大右邊界迴文子串的右邊界位置,pi記錄該回文字符串對稱軸位置:

如上如所示,i和j關於pi對稱,若在上述擁有最大有邊界的藍色文串範圍內,以j爲對稱軸處有一個迴文子串,那麼i爲對稱軸處也必然有一個迴文子串(關於pi的兩側是對稱的)。這樣便可減少當前i對稱軸的迴文比較次數,即j處會問半徑爲p[j],那麼i對稱軸會問判斷就直接從i+p[j]-1開始判斷(說明:公式i的對稱位置j=2*pi-i;);

但是有另外一種情況,就是j的一部分超出藍色部分,這時p[i]=p[j]就不一定對了,在使用i+p[j]-1前就一定要進行範圍判定,如下圖 :

實現代碼:
int maxLen3(string str)
{
	string s;
	//添加輔助符#
	s.push_back('#');
	for(int k=0;k<str.size();k++){
		s.push_back(str[k]);
		s.push_back('#');
	}
	cout<<s<<endl;

	//依次把每個字符作爲對稱中心進行最長迴文判斷
    int len = s.size();   
	//以下爲相比野蠻改進法新增的參數
    int *p = new int[len];  //輔助數組(記錄每個點爲對稱軸的迴文長度)
    p[0] = 1;
 	int mx =0, pi=0;//邊界和對稱中心
    for(int i=1;i<len-1;i++)	//s的首尾都是#,不屬於原字符串,可不計算
	{  
        if(mx>i)
        {
            p[i]=min(mx-i+1,p[2*pi-i]);//核心
        }else{
            p[i]=1;
        }

        while(i-p[i]>=0&&i+p[i]<=len-1 && s[i-p[i]]==s[i+p[i]]){
            p[i]++;
        }

        if(i+p[i]-1 > mx){
            mx = i+p[i]-1;
            pi = i;
        }
    }
    //最大回文字符串長度
	int maxlen = 0;
    for(int i=1;i<len-1;i++)
    {
        if(p[i]>maxlen)
        {
            maxlen = p[i];
        }
    }
    delete []p;
    return maxlen-1;
}

注意比較maxLen2和maxLen3的區別:主要就是將maxLen2中的count,在maxLen3中用數組存儲起來了,用於後面減少迴文子串開頭重複的比較次數;

(3)一個字符串最少需要刪掉幾個字符才能構成成迴文串:
解釋:求刪除最少元素後的最大回文串實際就是求取一個字符串的最大回文序列,故可用動態規劃方式
過程:
       首先,是將該字符串逆序,採用#include<algorithm>庫的reverse(s.begin(),s.end())函數得到逆序的s;
       然後,然後對原字符串s0和逆序字符串s採用如下計算字符串相同子串序列公式(動態規劃公式)進行判斷:
                 A=a1a2……aN,表示A是由a1a2……aN這N個字符組成,Len(A)=N;
                 B=b1b2……bM,表示B是由b1b2……bM這M個字符組成,Len(B)=M.
                定義LCS(i,j)=LCS(a1a2……ai,b1b2……bj),其中0≤i≤N,0≤j≤M.
                對於1≤i≤N,1≤j≤M,有公式:
                若ai=bj,則LCS(i,j)=LCS(i-1,j-1)+1;
                若ai≠bj,則LCS(i,j)=Max(LCS(i-1,j-1),LCS(i-1,j),LCS(i,j-1));
      最後,對於長度爲len的字符串,最後返回len-LCS(len,len)就是最少需要刪除的字符數。

對於上述相同子序列計算(動態規劃)公式的應用,需要設置一個矩陣(代碼中用二維數組表示)來進行統計,該部分參考代碼如下:
注意: MaxLen的大小應該是(length1+1)*(length2+1),字符串下標0對應MaxLen中的下標1,而MaxLen下標0的數據作爲輔助求取MaxLen中下標1的元素,從而保證MaxLen[i - 1][j], MaxLen[i][j - 1]不會溢出;可參見圖片:
#include<iostream>
#include<vector>
#include<string>
#include<algorithm>
using namespace std;

int maxLen(string s1, string s2){
    int length1 = s1.size();
    int length2 = s2.size();
	vector<vector<int> > MaxLen(length1+1,vector<int>(length2+1));  //也可以用指針來定義二維動態數組

    for (int i = 1; i <= length1; ++i)
    {
        for (int j = 1; j <= length2; ++j)
        {
            if (s1[i-1] == s2[j-1]){
                MaxLen[i][j] = MaxLen[i-1][j - 1] + 1;
            }
            else{
                MaxLen[i][j] = max(MaxLen[i - 1][j], MaxLen[i][j - 1]);
            }
        }
    }
    return MaxLen[length1][length2];
}

int main(){
	string str;
	while(cin>>str){
		string str0=str;
		reverse(str.begin(),str.end());
		cout<<maxLen(str0,str)<<endl;
	}
}


11、素數統計

答:有兩種方法:篩選法和開根號法;
篩選法:從小到大篩去一個已知素數的所有倍數。依次刪除可被2整除,3整除……的數字,剩下的則爲素數 。

開根號法:如果一個數大於2,對這個數求平方根,如果這個數能被這個數的平方根到2之間的任何一個(只要有一人就行)整除說明就不是質數,如果不能就說明是質數(開根號法時間複雜度小一些,只有篩選法的一半)。

篩選法是原理:所有非素數都是素數的乘積構成的;

開根號法原理:假如一個數N是合數,它有一個約數a,a×b=N,則a、b兩個數中必有一個大於或等於根號N,一個小於或等於根號N。因此,只要小於或等於根號N的數(1除外)不能整除N,則N一定是素數。

總結:兩者原理其實大同小異,後者是在前者基礎上改進而來。

篩選法舉例:
void getPrime0(int n){    
     int i,j;    
     bool m;    
     for(i = 1; i <= n; i ++){    
           m = true;    
           for(j = 2; j < i; j ++){    
                 if(i % j == 0){    
                      m = false;    
                      break;    
                 }    
           }    
           if(m){    
                 cout << i << " ";     
           }    
     }    
     cout << endl;    
}
事實上,上述j應該換爲n前的所有素數集合中的一員,所以需要數據存取操作,此處爲了方便沒有用該操作,而是直接使用所有的n前數據。

開根號法舉例:
bool prime(int x)  
{  
     int y;  
     for(y=2;y<=sqrt(x);y++)  
         if (x%y==0)  
            return false;  
     return true;  
}
以上是使用開根號算法的素數判斷函數。

12、小明走臺階問題——一個樓梯有n級,小明一次最多跨3級,問小明走完臺階有多少種走法?

答:(1)這是一個遞歸問題,但可以用循環實現:
首先,假設走法總數爲f(n),那麼f(1)=1,f(2)=2,f(3)=4;
當n>3,有如下遞歸關係:
f(n)=f(n-1)+f(n-2)+f(n-3),因爲把爬n級臺階的最後一步分類,則f(n-1)代表最後一步是爬1級的所有走法,f(n-2)代表最後一步是爬2級的所有走法,f(n-3)代表最後一步是爬3級的所有走法,因此關係式成立。所以:
n>3,f(n)=f(n-1)+f(n-2)+f(n-3);
注意:這是一次最多跨三級,如果只能跨兩級就需要做相應改變。
f(1)=1
f(2)=2
f(3)=4
f(4)=7
f(5)=2*7-f(1)=13
f(6)=2*13-f(2)=24
f(7)=2*24-f(3)=44
f(8)=88-f(4)=81
f(9)=2*81-f(5)=149
f(10)=298-f(6)=274
f(11)=548-f(7)=504
f(12)=1008-f(8)=927
f(13)=1854-f(9)=1854-149=1705
f(14)=3410-f(10)=3410-274=3136
f(15)=6272-f(11)=6272-504=5768
……

代碼實現(一個循環語句搞定):
int climbStairs(int n){
    vector<int> v;
    v.push_back(1);
    v.push_back(1);
    for(int i = 2; i <= n; i++){
		v.push_back(v[i - 1] + v[i - 2]);
    }
    return v[n];
}
(2)斐波那契查找
斐波那契查找是對斐波那契數列進行查找,斐波那契序列非常類似上述走臺階的序列:
F0=0,F1=1,Fn=Fn-1+Fn-2(n>=2);

13、給定n個數的進棧序列,求出棧序列有多少種類型——卡特蘭數

答:n個數有多少種出棧序列,用卡特蘭數求:

f(n)=f(0)f(n-1)+f(1)f(n-2)+f(2)f(n-3)+……+f(n-2)f(1)+f(n-1)f(0);其中,f(0)=f(1)=1;

所以,如果有一入棧序列爲e1,e2,e3,e4,e5,那麼出棧序列就有f(5)=42種。


14、快慢指針——判斷循環鏈表及其他

答:快慢指針中的快慢指的是移動的步長,即每次向前移動速度的快慢。例如可以讓快指針每次沿鏈表向前移動2,慢指針每次向前移動1次。

(1)快慢指針可以用於判斷單循環鏈表:讓快慢指針從鏈表頭開始遍歷,快指針向前移動兩個位置,慢指針向前移動一個位置:

1)如果快指針到達NULL,說明鏈表以NULL爲結尾,不是循環鏈表;

2)如果 快指針追上慢指針,即快指針=慢指針,則表示出現了循環。

3)爲什麼慢指針步長爲1的話,快指針步長就爲2:因爲只有fastStep-slowStep=1,才能實現快慢指針一定相遇,而不是快指針越過慢指針。

代碼實現如下:

int isExitsLoop(LinkList* L) {
    LinkList *fast, *slow;
    fast = slow = L;
    while (fast!=NULL && fast->next!=NULL)
    {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast)
        {
            break;
        }
    }
    return ((fast == NULL) || (fast->next == NULL));
}

快慢指針用於判斷有無環,有時候還要判斷環的入口位置(尤其是單鏈表局部環入口),這種情況參考另一博文:http://blog.csdn.net/xiongchao99/article/details/74524807#t15

(2)快慢指針獲取鏈表中間節點

使用快慢指針同時出發,當快指針到達終結點時慢指針剛好到達中間節點。


15、尋找二叉樹中兩個節點的最近祖先節點

答:1)若二叉樹是二叉排序樹(或叫做二叉查找樹、二叉搜索樹):

直接前序遍歷,找到一個節點m,滿足n1<m<n2,其中n1、n2是給定的兩個節點。

2)若爲普通二叉樹:

①樹節點結構體中給定父節點指針

如:

struct node{

Node * left; 
Node * right; 
Node * parent; 

}

則算法思想:首先給出p的父節點p->parent,然後將q的所有父節點依次和p->parent作比較,如果發現兩個節點相等,則該節點就是

最近公共祖先,直接將其返回。如果沒找到相等節點,則將q的所有父節點依次和p->parent->parent作比較,直到p->parent==root。

程序實現如下:

Node * NearestCommonAncestor(Node * root,Node * p,Node * q)  
{  
    Node * temp;  
         while(p!=NULL)  
    {  
        p=p->parent;  
        temp=q;  
        while(temp!=NULL)  
        {  
            if(p==temp->parent)  
                return p;  
            temp=temp->parent;  
        }  
    }  
}  

②若未給定父節點指針

算法思想:如果一個節點的左子樹包含p,q中的一個節點,右子樹包含另一個,則這個節點就是p,q的最近公共祖先。

程序實現:

/*查找a,b的最近公共祖先,root爲根節點,out爲最近公共祖先的指針地址*/  
int FindNCA(Node* root, Node* a, Node* b, Node** out)   
{   
    if( root == null )   
    {   
        return 0;   
    }  
  
    if( root == a || root == b )  
    {      
        return 1;  
    }  
  
    int iLeft = FindNCA(root->left, a, b, out);  
    if( iLeft == 2 )  
    {      
        return 2;  
    }  
  
    int iRight = FindNCA(root->right, a, b, out);  
    if( iRight == 2 )  
    {      
        return 2;  
    }  
  
    if( iLeft + iRight == 2 )  
    {     
        *out = root;  
    }  
    return iLeft + iRight;  
}  
用遞歸方式實現對樹一層一層的訪問,若left+right=2,那麼表示當前節點就是最近祖先節點。


16、雙棧排序

答:例如,實現棧數據的升序排列,即棧頂數據最大。

思路:利用一個輔助棧,每次比較排序棧和輔助棧的頂元素,如果排序棧較小直接壓入輔助棧,並彈出排序棧,否則將輔助棧的元素彈出並壓在排序棧棧頂元素的後面。如此反覆,直到排序棧沒有元素了,之後將輔助棧的元素全部導入排序棧,就完成排序。

程序實現:

class TwoStacks {
public:
    vector<int> twoStacksSort(vector<int> numbers) {
            stack<int> mystack,help;
            for(auto i=numbers.end()-1;i>=numbers.begin();--i)
                mystack.push(*i);           
            while(!mystack.empty())
                {
                if(help.empty()){
                    help.push(mystack.top());
                    mystack.pop();
                }
                else if(mystack.top()<=help.top())
                    {
                    help.push(mystack.top());
                    mystack.pop();
                }
                else
                    {
                    int temp=mystack.top();
                    mystack.pop();
                    mystack.push(help.top());
                    mystack.push(temp);
                    help.pop();
                }
            }
        while(!help.empty())
            {
            mystack.push(help.top());
            help.pop();
        }
        for(auto &c:numbers)
            {
            c=mystack.top();
            mystack.pop();
        }
        return numbers;
    }
};


17、判斷一個二叉樹結構是否爲另一個二叉樹的子結構

答:一般算法分爲兩個步驟:

(1)第一步在樹A中找到和B的根節點的值一樣的結點R;
(2)第二步再判斷樹A中以R爲根結點的子樹是不是包含和樹B一樣的結構。

C++實現代碼如下:

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
    {
            bool flag=false;
            
            if(pRoot1!=NULL && pRoot2!=NULL)
            {
                if(pRoot1->val==pRoot2->val)	//判斷根節點,相等就繼續判斷其他節點是否相等
                    flag=Match(pRoot1,pRoot2);
                if(!flag)		//否則判斷左兒子節點是否與子結構根節點相等
                	flag=HasSubtree(pRoot1->left,pRoot2);
            	if(!flag)		//還不等,則判斷右兒子節點是否與子結構根節點相等
                	flag=HasSubtree(pRoot1->right,pRoot2);
            }            	
            return flag;
    }
    

    
	bool Match(TreeNode* root1,TreeNode* root2){
        if(root1 == NULL && root2 != NULL) return false;
        if(root2 == NULL) return true;
        if(root1->val != root2->val) return false;
        //if(root1->val == root2->val)
        return Match(root1->left, root2->left)&&Match(root1->right, root2->right);
    }
};


18、DFS

答:以二叉樹爲例(就是二叉樹的先根遍歷),其他樹或圖的DFS在此基礎上進行改進。實現代碼如下:

①遞歸方式十分簡單:

void preorder(TreeNode root){  
    if(root){  
        cout<<root->data<<' ';  
        preorder(root->lchild);  
        preorder(root->rchild);  
    }  
} 

②非遞歸方式:需要使用棧作爲輔助,兩個循環實現;

        while(t || s.empty!=True){  
            while(t){    //只要結點不爲空就應該入棧保存,與其左右結點無關      
                cout<<t->data<<' ';  
                push(&s,t);  
                t= t->lchild;  
            }  
            t=pop(&s);  
            t=t->rchild;  
        }  


19、BFS——隊列輔助

答:以二叉樹爲例(就是二叉樹按層遍歷),其他樹或圖的BFS在此基礎上進行改進。使用隊列queue實現,每當從隊列頭部彈出一個節點,就將該節點的子節點按先左後右的方式壓入隊尾;實現代碼如下:

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    vector<int> PrintFromTopToBottom(TreeNode* root) {
        vector<int> v;
		queue<TreeNode*> q;
        if(root==NULL)
            return v;
        
        q.push(root);       
        while(q.size()>0){
            
            int data=q.front()->val;           
            v.push_back(data);
            
            if(q.front()->left!=NULL)
                q.push(q.front()->left);
            if(q.front()->right!=NULL)
                q.push(q.front()->right);
            q.pop();
        }
        return v;
    }
};


20、貪心算法

答:所謂貪心算法是指總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的僅是在某種意義上的局部最優解。
一、貪心算法的基本思路:
1.建立數學模型來描述問題。
2.把求解的問題分成若干個子問題。
3.對每一子問題求解,得到子問題的局部最優解。
4.把子問題的解局部最優解合成原來解問題的一個解。

二、貪心算法適用的問題
貪心策略適用的前提是:局部最優策略能導致產生全局最優解。
實際上,貪心算法適用的情況很少。一般,對一個問題分析是否適用於貪心算法,可以先選擇該問題下的幾個實際數據進行分析,就可做出判斷。
例如,常見的揹包問題對於貪心算法看似適用,其實是不適用,比如:物品A、B、C的價值爲30、20、10,質量爲30、20、10。若揹包最大承重爲28,那麼按照貪心算法應該取單位質量價值最高者,但三者價質比都爲1,所以無法選擇,若任意選擇,選A錯,選C不是最佳結果;

三、貪心算法的實現框架
如下:
從問題的某一初始解出發;
while (能朝給定總目標前進一步)

利用可行的決策,求出可行解的一個解元素;
}
由所有解元素組合成問題的一個可行解;

四、常見的貪心算法應用
(1)最小生成樹的Prim算法和Kruskal算法
最小生成樹:包括了加權連通圖裏的所有頂點,且其所有邊的權值之和亦爲最小;

prim算法搜索最小生成樹:
設加權圖的頂點集爲V,邊集爲E,先新建一個頂點集Vnew和邊集Enew,主要步驟就是重複下列操作,直到Vnew = V:
a在集合E中選取權值最小的邊<u, v>,其中u爲集合Vnew中的元素(已經選擇過的舊頂點),而v不在Vnew集合當中(如 果存在有多條滿足前述條件即具有相同權值的邊,則可任意選取其中之一)
b、將v加入集合Vnew中,將<u, v>邊加入集合Enew中;

Kruskal算法搜索最小生成樹:
1)記Graph中有v個頂點,e個邊;
2)新建圖Graphnew,Graphnew中擁有原圖中相同的e個頂點,但沒有邊;
3)將原圖Graph中所有e個邊按權值從小到大排序;
4)循環:從權值最小的邊開始遍歷每條邊直至圖Graph中所有的節點都在同一個連通分量中,如果這條邊連接的兩個節點不在同一個連通分量中,添加這條邊到圖Graphnew中;

(2)均分紙牌
有n堆紙牌,每堆紙牌數量不等,每次從任一堆中選取若干張紙牌,移到自己相鄰的堆中。例如:有4個堆,分別有9、8、17、6張紙牌,現進行移動最少次數使得每堆紙牌數相等;

算法:
從左向右遍歷,當有一堆不足均值,就從右邊相鄰的取,大於均值就將多餘的紙牌移動到右邊相鄰的堆上;

如上舉例:
①從②取1張,②從③取三張,③向④送4張,④不變,結束;用了三次移動,是最少的情況。

(3)最大整數
設有n個正整數,將它們連接成一排,組成一個最大的多位整數。
例如:n=3時,3個整數13,312,343,連成的最大整數爲34331213。
又如:n=4時,4個整數7,13,4,246,連成的最大整數爲7424613。

算法:
從左到右,依次進行“先把整數轉換成字符串,然後在比較a+b和b+a,如果a+b>=b+a,就把a排在b的前面,反之則把a排在b的後面”。
如上舉例:
"13312"<"31213",故取結果爲後者,接着"31213343"<"34331213",也取結果爲後者;

21、比較鑽石重量(筆試編程題)

答:題目如下:
小明陪小紅去看鑽石,他們從一堆鑽石中隨機抽取兩顆並比較她們的重量。這些鑽石的重量各不相同。在他們們比較了一段時間後,它們看中了兩顆鑽石g1和g2。現在請你根據之前比較的信息判斷這兩顆鑽石的哪顆更重。
給定兩顆鑽石的編號g1,g2,編號從1開始,同時給定關係數組vector,其中元素爲一些二元組,第一個元素爲一次比較中較重的鑽石的編號,第二個元素爲較輕的鑽石的編號。最後給定之前的比較次數n。請返回這兩顆鑽石的關係,若g1更重返回1,g2更重返回-1,無法判斷返回0。輸入數據保證合法,不會有矛盾情況出現。
測試樣例:2,3,[[1,2],[2,4],[1,3],[4,3]],4
返回:1

思路:設法將大於g1的所有元素存入max中,小於g1的元素存入min中,然後查看g2在max還是min中;
實現代碼如下:
int cmp(int g1, int g2, int records[][2], int n) 
{  
	// write code here          
	vector<int> max, min;  		
	//先直接根據含有g1的比較組,將大於g1的值添加到數組max,小於g1的值添加到數組min       
	for (int i = 0; i < n; i++) 		
	{            
		if (records[i][0] == g1) 
		{                  
			min.push_back(records[i][1]);              
		}  
            
		if (records[i][1] == g1) 
		{                  
			max.push_back(records[i][0]);              
		}          
	}  

    //然後根據重量大小關係的傳遞性,循環比較,向max中添加比max已有元素還大的值,向min中
	//添加比min已有元素還小的值
	int count = 0;          
	while (count < n) //爲什麼要比較n輪:最壞的情況是每輪只有比較序列中最後一對某元素被添加,故
	{                //需要n輪纔可以保證添加完整性
		count++;             
		for (int i = 0; i < n; i++) 			
		{                 
			if (records[i][0] != g1 && records[i][1] != g1) 
			{  					
				if (find(min.begin(),min.end(),records[i][0])!=min.end()) //原有min數組中發現當前數值對較大者
					min.push_back(records[i][1]);
				if (find(max.begin(),max.end(),records[i][1])!=max.end()) //原有max數組中發現當前數值對較小者
					max.push_back(records[i][0]);
			}    
		}     
	}  
  
    if (find(max.begin(),max.end(),g2)!=max.end() && find(min.begin(),min.end(),g2)==min.end())           
		return -1;   			
	else if (find(max.begin(),max.end(),g2)==max.end() && find(min.begin(),min.end(),g2)!=min.end())           
		return 1; 		
	else              
		return 0;          
}

22、任意進制之間互相轉換

答:題目描述
將一個處於Integer類型取值範圍內的整數從指定源進制轉換爲指定目標進制; 可指定的進制值範圍爲[2,62]; 
每個數字位的可取值範圍爲[0-9a-zA-Z]; 輸出字符串的每一個都須爲有效值;反例:"012"的百位字符即爲無效值。 實現時無需考慮非法輸入。

輸入描述:
輸入爲:
源進制 目標進制 待轉換的整數值
例子:8 16 12345670

輸出描述:
整數轉換爲目標進制後得到的值

輸入例子:
8 16 12345670

輸出例子:
29cbb8

思路:這類進制轉換的題目一般是先轉化爲十進制,然後將十進制轉化爲目標進制(即:以十進制作爲橋樑)
代碼實現:
#include<iostream>
#include<string>
using namespace std;

int main(){
	int source,target=0;
	string str;
	while(cin>>source>>target>>str){
		int DecNum=0;
		//區分正負數
		int i=0;
		if(str[0]=='-')
			i=1;
		else
			i=0;

		//轉化爲10進制
		while(i<str.size()){
			int num=0;
			DecNum=DecNum*source;
			if(str[i]<='9')
				num=str[i]-'0';
			else if(str[i]>='a' && str[i]<='z')
				num=str[i]-'a'+10;
			else if(str[i]>='A' && str[i]<='Z')
				num=str[i]-'A'+36;				

			DecNum+=num;
			i++;
		}

		//10進制轉化爲目標進制
		string tStr;
		while(DecNum>0){
			string temStr;
			int num=DecNum%target;
			if(num<=9)
				temStr=std::to_string(static_cast<long long>(num));	//VS2010未實現int轉化爲string
			else if(num>=10 && num<=35)
				temStr='a'+num-10;
			else if(num>=36 && num<=61)
				temStr='A'+num-36;
			tStr=temStr+tStr;

			DecNum=DecNum/target;
		}
		if(str[0]=='-')
			tStr="-"+tStr;
		cout<<tStr<<endl;
	}
}

23、鏈表反轉

答:鏈表反轉最典型的的方式就是使用三個指針p、q、r,前兩個用於當前反轉的兩個節點,第三個用於保存後續待反轉的第一個節點。注意,r指針的必須的,否則q的反轉會造成q->next丟失。實現代碼如下:
ListNode* ReverseList(ListNode* pHead) {
    ListNode *p,*q,*r;
	if(pHead==NULL || pHead->next==NULL){
		return pHead;
	}else{
		p=pHead;
		q=p->next;
		pHead->next=NULL;
		while(q!=NULL){
			r=q->next;
			q->next=p;
			p=q;
			q=r;
		}
		return p;
	}
}

24、統計二進制中有多少個1

答:這類算法題是非常普遍的,解法也有多種,在此介紹3種(注意:以下算法對正負整型數都適用):
①直接統計二進制每位是否爲1(用求餘法,和十進制統計每位一樣)
int Count1(unsigned int v)
{
    int num = 0;    
    while(v)
    {
         if(v % 2 == 1)
         {
              num++;  
         }
         v = v/2;
    }  
    return num;
}

②右移法(每次右移前統計最後一位是否爲1)
int Count2(unsigned int v)
{
    unsigned int num = 0;  
    while(v)
    {
         num += v & 0x01;
         v >>= 1;
    }
    return num;
}

③依次清除最右位1法(使用減1和&實現清除,每清除一次統計數加一)
int Count3(unsigned int v)
{
    int num = 0;    
    while(v)
    {
         v &= (v-1);
         num++;
    }
    return num;
}

25、複雜鏈表複製

答:複雜鏈表:每個節點中有節點值,以及兩個指針,一個指向下一個節點,另一個特殊指針指向任意一個節點。如下:
struct RandomListNode {
    int label;
    struct RandomListNode *next, *random;
    RandomListNode(int x) :
    label(x), next(NULL), random(NULL){
}
};
複雜鏈表複製就是指複製一個一模一樣的鏈表,不是淺拷貝而是深拷貝。因爲每個節點多了一個指向任意位置的特殊指針,常見的鏈表深拷貝方式(按節點對應關係一個個創建)已經不好用了,要實現複製複雜鏈表,最好的辦法是“將新創建的節點插入到原鏈表對應節點的後面”,具體如下:
     1)複製節點A得到A1,將A1插入節點A後面;
     2)遍歷鏈表,A1->random = A->random->next;
     3)將鏈表拆分成原鏈表和複製後的鏈表;
代碼實現:
鏈接:https://www.nowcoder.com/questionTerminal/f836b2c43afc4b35ad6adc41ec941dba
來源:牛客網

    RandomListNode* Clone(RandomListNode* pHead)
    {
        if(!pHead) return NULL;
        RandomListNode *currNode = pHead;
        while(currNode){
            RandomListNode *node = new RandomListNode(currNode->label);
            node->next = currNode->next;
            currNode->next = node;
            currNode = node->next;
        }
        currNode = pHead;
        while(currNode){
            RandomListNode *node = currNode->next;
            if(currNode->random){                
                node->random = currNode->random->next;
            }
            currNode = node->next;
        }
        //拆分
        RandomListNode *pCloneHead = pHead->next;
        RandomListNode *tmp;
        currNode = pHead;
        while(currNode->next){
            tmp = currNode->next;
            currNode->next =tmp->next;
            currNode = tmp;
        }
        return pCloneHead;
    }

26、兩個有序鏈表合併

答:題目:輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,當然我們需要合成後的鏈表滿足單調不減規則。
思路:針對兩個有序鏈表或者線性表的合併問題,一般都是定義一個數組(或者vector)v,然後根據:
while(i<a.size() && j<b.size()){
     if(a[i]>=b[j]){
          v.push_back(a[i]);
    	  i++;
     }
     else{
          v.push_back(b[j]);
          j++;
     }
}

if(i<a.size()){
     ……
}
else if(b.size()){
     ……
}
分別將兩組數據按大小順序插入新的數組或容器中;
上述題目代碼實現:
/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
    {
        if(pHead1==NULL){
            return pHead2;
        }else if(pHead2==NULL){
            return pHead1;
        }
            
        ListNode* pHead;
        vector<ListNode*> v;
        int len=0;
        while(pHead1!=NULL && pHead2!=NULL){
			if(pHead1->val<=pHead2->val){
                v.push_back(pHead1);
                pHead1=pHead1->next;
            }else{
                v.push_back(pHead2);
                pHead2=pHead2->next;
            }
            
            len=v.size();
            if(len>=2){
                v[len-2]->next=v[len-1];
            }
        }
        
        if(pHead1!=NULL){
            v[len-1]->next=pHead1;
        }else if(pHead2!=NULL)
            v[len-1]->next=pHead2;
        
        pHead=v[0];
        return pHead;
    }
};
拓展:上述是常規方式,針對兩個有序鏈表的合併,還可以使用遞歸實現:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
	if(pHead1==NULL)
		return pHead2;
	else if(pHead2==NULL)
		return pHead1;
	if(pHead1->val <= pHead2->val){
		pHead1->next=Merge(pHead1->next,pHead2);
		return pHead1;
	}else{
		pHead2->next=Merge(pHead1,pHead2->next);
		return pHead2;
	}
}
遞歸太強大,代碼量少多了。。。

27、字符串全排列

答:全排列:對於一個序列,按照一定的排列規則(一般都是按元素字典順序)進行排序,獲得所有的排序序列。若序列有n個元素,全排列就有n!個序列組合;
舉例:字符串acb的全排列就是從最小的abc開始已進行排序得到abc、acb、bac、bca、cab、cba共3!=6個;
實現方式:
(1)調用庫函數:next_permutation:
對於C++的使用者,STL中的algorithm庫封裝了全排列函數next_permutation(str.begin(), str.end()),具體實現如下:
    vector<string> Permutation(string str){        
        vector<string> v;        
        if(str=="")
	    return v;
        sort(str.begin(), str.end());//必須先遞增排序,使得str是最小一種排序		
        do
        {		
            v.push_back(str);		
        }
        while (next_permutation(str.begin(), str.end()));//每一次循環返回true表示找到了比str大的字符串

        return v;
    }

prev_permutation:
     prev_permutation和next_permutation的區別就是:next_permutation獲得序列集合是按字典序或者升序進行的,而prev_permutation正好相反,是按降序獲取的。例如字符串abc調用prev_permutation後返回FALSE,因爲abc已經是最小組合了,acb字符串調用後就是abc;降序全排列代碼如下:
bool cmp(const char a,const char b){
	return a>b;
}

vector<string> Permutation(string str){        
	vector<string> v;        
	if(str=="")
	     return v;
	sort(str.begin(), str.end(),cmp);//此處不同		
	do
	{		
	     v.push_back(str);		
	}
	while (prev_permutation(str.begin(), str.end()));

    return v;
}
注意,next_permutation的參數分別是:第一個地址和最後元素的下一個地址,例如上述程序str.begin(), str.end();或直接使用str,str+str.size()即可。

(2)自己編碼:
一般分兩種:非遞歸和遞歸;
①非遞歸(字典序法):
【例】 如何得到346987521的下一個
1,從尾部往前找第一個P(i-1) < P(i)的位置
4 6 <- 9 <- 8 <- 7 <- 5 <- 2 <- 1
最終找到6是第一個變小的數字,記錄下6的位置i-1
2,從i位置往後找到最後一個大於6的數
4 6 -> 9 -> 8 -> 7 5 2 1
最終找到7的位置,記錄位置爲m
3,交換位置i-1和m的值
4 7 9 8 6 5 2 1
4,倒序i位置後的所有數據
4 7 1 2 5 6 8 9
則347125689爲346987521的下一個排列;
算法實現代碼:
vector<string> permutation(string str)
{           
	vector<string> v;
	if(str.empty())
		return v;
	int length=str.size();
	int fromIndex, changeIndex;          
	sort(str.begin(), str.end());  //先升序排列,獲取最小的字符串組合
	do        
	{           
		//保存當前獲得的一種全排列組合
		v.push_back(str);   

		fromIndex = length - 1;        
		//(1)向前查找第一個由大變小的元素位置          
		while (fromIndex > 0 && str[fromIndex] <= str[fromIndex - 1]) 
			--fromIndex;             
		changeIndex = fromIndex;             
		if (fromIndex == 0) 
			break;
		//(2)向後查找最後一個大於words[fromIndex-1]的元素             
		while (changeIndex + 1< length && str[changeIndex + 1] >= str[fromIndex - 1]) 
			++changeIndex;
		//(3)交換兩個值  
		swap(str[fromIndex - 1], str[changeIndex]);        
		//(4)對後面的所有值進行反向處理  
		reverse(str.begin()+fromIndex, str.end());           
	} 
	while (true);
	return v;
 }
此方式在VS2010上可以實現,但不知道爲什麼牛客網上提示內存超限制,空間複雜度過大;

②遞歸方式
遞歸方法很容易理解:分別將每個位置交換到最前面位,之後全排列剩下的位。
【例】遞歸全排列 1 2 3 4 5
1,for循環將每個位置的數據交換到第一位
swap(1,1~5);
2,按相同的方式全排列剩餘的位;
可見下圖:

代碼實現:
void PermutationHelp(vector<string> &ans, int k, string str) //遍歷第k位的所有可能
    {
        if(k == str.size() - 1)
            ans.push_back(str);
        for(int i = k; i < str.size(); i++)
        {
            if(i != k && str[k] == str[i])
                continue;
            swap(str[i], str[k]);
            PermutationHelp(ans, k + 1, str);
        }
    }
 
    vector<string> Permutation(string str) {
        sort(str.begin(), str.end());
        vector<string> ans;
        PermutationHelp(ans, 0, str);
        return ans;
    }

28、單鏈表的反轉算法

答:思想:創建3個指針,分別指向上一個節點、當前節點、下一個節點,遍歷整個鏈表的同時,將正在訪問的節點指向上一個節點,當遍歷結束後,就同時完成了鏈表的反轉。

實現代碼:

    ListNode* ReverseList(ListNode* pHead) {
        ListNode *p,*q,*r;
        if(pHead==NULL || pHead->next==NULL){
            return pHead;
        }else{
            p=pHead;
            q=p->next;
            pHead->next=NULL;
            while(q!=NULL){
                r=q->next;
                q->next=p;
                p=q;
                q=r;
            }
            return p;
        }
    }


29、棧作爲輔助結構的經典算法

答:(1)括號匹配檢驗:若是左括號就壓入棧,若是右括號就將棧頂括號彈出與右括號匹配;
(2)迷宮求解:若當前位置可通就壓入棧,接着向下一個位置探索。若當前位置不可通,則讀取棧頂元素,繼續探索該元素其他方向的鄰接位置。重複上述過程直到出口位置。
(3)算術表達式求值:使用兩個棧,一個用來寄存運算符,另一個用來寄存操作數和運算結果。基本思想如下:

(4)實現一個擁有min()成員函數的棧結構:使用兩個棧,其中一個用於存儲所有元素,另一個用於存儲每個序列對應的最小值。基本思想如下:
①當前要進棧元素<=stackMin棧頂元素時,將當前要進棧元素同時加入到stackMin中;
②當前要進棧元素>stackMin棧頂元素時,stackMin棧把當前stackMin的棧頂元素再壓入一遍;
(5)兩個棧實現隊列的先進先出;
(6)不使用第二個棧作爲輔助的情況下實現棧的反轉:是用遞歸實現,每次遞歸創建一個局部變量存儲一個棧元素。基本思路如下:
①遞歸獲取棧底元素,將棧底元素取出,其他元素依次在退出遞歸時按原順序壓入棧;
②將上述步驟①迭代n-1次就可實現棧的反轉;
獲取棧底元素(步驟①)參考代碼:
int popBottom(Stack<Integer> stack){  
     int result = stack.pop();  
     if(stack.isEmpty()){//彈出一個棧頂元素後,棧爲空了,表示該元素就是棧底元素  
         return result;  
     }else{  
         int last = popBottom(stack);  
         stack.push(result);//注意!!!這裏是把前面拿到的元素壓入,這樣棧底元素纔不會再次壓入到棧中  
         return last;  
     }  
 }  
(6)棧中元素排序(最多使用一個輔助棧):假設棧stack是存放原來數據的,再定義一個輔助棧help,先從stack棧中取出棧頂元素pop,將pop和help中棧頂元素比較,如果pop <= help棧頂元素,將pop壓入到help棧中;如果pop > help棧頂元素,取出help棧頂元素,將其放入到stack棧中,直到help爲空或者pop <= help棧頂元素。

30、隊列作爲輔助結構的經典算法

答:(1)兩個隊列實現棧的先進後出;
(2)樹的廣度優先遍歷:先向隊列壓入第一層節點(只有一個根節點),再彈出隊列頭部節點(根節點)並打印,彈出節點的同時將該節點的所有子節點從左到右依次壓入隊列。接着又彈出頭部節點並打印,同時把該節點的所有子節點都壓入隊列。以此類推。
(3)雙端隊列實現滑動窗口最大值序列獲取:雙端隊列保存的是數組的下標,通過插入與彈出保證隊首始終是最值的下標,並且時間複雜度爲O(n)。基本思路:
①窗口向前移動一個元素,先在隊頭執行彈出規則:若前隊尾-隊首元素的值==窗口大小w-1,就表示隊頭元素在這一輪就過期了,直接將隊頭元素彈出;
在隊尾執行插入規則:1°、隊列爲空肯定直接插入;2°、隊列不空,如果隊尾元素arr[qmax.peekLast]  > 當前遍歷元素arr[i],直接將下標i插入到隊尾;如果隊尾元素爲下標所指的數組元素arr[qmax.peekLast] <= 當前遍歷元素arr[i],說明當前隊尾元素下標不可能成爲後面窗口的最大值了,因此直接將隊尾元素彈出,再繼續比較新的隊尾元素所指數組元素和當前元素arr[i],根據上面規則加入;

31、將二叉搜索樹轉化爲雙向鏈表,不允許創建新節點和其他數據結構輔助

答:代碼如下(自己寫的,可能不夠簡潔但思想基本如此:無論左右子樹,都返回子樹中最大的節點(左子樹最大節點是左子樹根,右子樹最大節點是右子樹最靠右的節點);當前節點與左/右子樹鏈接時注意:左子樹的最大節點要與當前節點鏈接,右子樹則是其最小節點要與當前節點鏈接):
	struct TreeNode {
		int val;
		struct TreeNode *left;
		struct TreeNode *right;
		TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
		}
	};

	TreeNode* Tree2List(TreeNode* pRootOfTree){
		TreeNode *Node;
		if(pRootOfTree->left==NULL && pRootOfTree->right==NULL)//葉子就返回,否則繼續下面的遞歸
			return pRootOfTree;
		//思想:無論左右子樹,都返回子樹中最大的節點(左子樹最大節點是左子樹根,右子樹最大節點是右子樹最靠右的節點)
		if(pRootOfTree->left!=NULL)
		{
			Node=Tree2List(pRootOfTree->left);
			pRootOfTree->left=Node;		//將當前節點鏈接在左子樹最大節點右邊
			Node->right=pRootOfTree;
			Node=pRootOfTree;	//(1)返回當前樹的最大節點(當前樹的根節點)
		}
		if(pRootOfTree->right!=NULL)
		{
			Node=Tree2List(pRootOfTree->right);//(2)子樹返回值(最大節點)
			TreeNode *temNode=Node;
			while(temNode->left!=NULL)//獲取右子樹最小節點
				temNode=temNode->left;
			pRootOfTree->right=temNode;//將當前節點鏈接在右子樹最小節點的左邊
			temNode->left=pRootOfTree;
		}
		return Node;	//最後返回當前樹的最大節點(無右子樹就返回根節點(1),有右子樹就返回右子樹最大節點(2))	
	}
    TreeNode* Convert(TreeNode* pRootOfTree)
    {        
		if(pRootOfTree==NULL)
			return NULL;
		TreeNode* node=Tree2List(pRootOfTree);
		while(node->left!=NULL)	//要求返回最左邊第一個節點,所以作如下操作
			node=node->left;
		return node;
    }

32、整數中1出現的次數

答:求出1~13的整數中1出現的次數,並算出100~1300的整數中1出現的次數?爲此他特別數了一下1~13中包含1的數字有1、10、11、12、13因此共出現6次,但是對於後面問題他就沒轍了。ACMer希望你們幫幫他,並把問題更加普遍化,可以很快的求出任意非負整數區間中1出現的次數。
結合下面代碼分析思路:分別對數值的每一位分別進行考慮,其次將數值每位分三種情況:大於等於2、等於1、等於0。具體如下:
(1)當m表示百位,且百位對應的數>=2,如n=31456,m=100,則a=314,b=56,此時百位爲1的次數有a/10+1=32(最高兩位0~31),每一次都包含100個連續的點,即共有(a%10+1)*100個點的百位爲1;
(2)當m表示百位,且百位對應的數爲1,如n=31156,m=100,則a=311,b=56,此時百位對應的就是1,則共有a/10(不加1,即最高兩位0-30)是包含100個連續點。當最高兩位爲31(即a=311),本次只對應局部點00~56,共b+1次,所有點加起來共有(a%10*100)+(b+1),這些點百位對應爲1;
(3)當m表示百位,且百位對應的數爲0,如n=31056,m=100,則a=310,b=56,此時百位爲1的次數有a/10=31(不加1,最高兩位0~30);  
綜合以上三種情況,可得代碼如下:
    int NumberOf1Between1AndN_Solution(int n)
    {
		int ones = 0;
		for (int m = 1; m <= n; m *= 10) {
			int a = n/m, b = n%m;
			ones += (a + 8) / 10 * m + (a % 10 == 1) * (b + 1);
		}
		return ones;
    }

33、把數組元素連接成最小的數值

答:題目:輸入一個正整數數組,把數組裏所有數字拼接起來排成一個數,打印能拼接出的所有數字中最小的一個。例如輸入數組{3,32,321},則打印出這三個數字能排成的最小數字爲321323。
思路:主要通過將數值轉化爲string,然後根據string比較大小的屬性進行連接。其中用到sort函數對string排序,其中排序規則需要自己重寫(比如string中3小於32,但本題要求32小於3,故需要重寫規則函數cmp)。
代碼如下:
static bool cmp(int a,int b){
	string A="";
	string B="";
	A=A+to_string((long long)a);
	A=A+to_string((long long)b);
	B=B+to_string((long long)b);
	B=B+to_string((long long)a);
	return A<B;
}
string PrintMinNumber(vector<int> numbers) {
	sort(numbers.begin(),numbers.end(),cmp);

	string resStr;
	for(int k=0;k<numbers.size();k++){
		resStr+=to_string((long long)numbers[k]);
	}
    return resStr;
}

34、獲取第n個醜數

答:題目:把只包含因子2、3和5的數稱作醜數(Ugly Number)。例如6、8都是醜數,但14不是,因爲它包含因子7。 習慣上我們把1當做是第一個醜數。求按從小到大的順序的第N個醜數。
思路:後面的醜數是有前一個醜數乘以2,3,5中的一個得來。因此可以用動態規劃去解。同時注意一下,題目意思應該是質數因子。結合下面代碼分析,res是存儲前面醜數的數組,t2、t3、t5是求取下一個醜數的候選基下標。每次總是取大於前一個醜數的三個最小丑數進行比較,取最小值爲當前醜數。
代碼如下:
int GetUglyNumber_Solution(int index) {
	vector<int> res(index);
	res[0] = 1;
	int t2 = 0, t3 = 0, t5 = 0, i;
	for (i = 1; i < index; ++i)
	{
		res[i] = min(res[t2] * 2, min(res[t3] * 3, res[t5] * 5));
		if (res[i] == res[t2] * 2)t2++;
		if (res[i] == res[t3] * 3)t3++;
		if (res[i] == res[t5] * 5)t5++;
	}
	return res[index - 1];
}

35、逆序對

答:逆序對問題:在數組中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數P。
思路:對於數組中逆序對問題,由於一般數組都較大,用暴力法時間複雜度O(n^2)不合適,一般都可以借用歸併排序思路,時間複雜度爲o(nlogn)。歸併實現求取逆序對的具體實現代碼就是在本博文前面所述的歸併排序代碼中添加計數語句即可。
代碼實現:
int mergeSort(vector<int>& data, int start, int end) {
	// 遞歸終止條件
	if(start >= end) {
		return 0;
	}
	// 遞歸
	int mid = (start + end) / 2;
	int leftCounts = mergeSort(data, start, mid);
	int rightCounts = mergeSort(data, mid+1, end);

	// 歸併排序,並計算本次逆序對數
	vector<int> copy(data); // 數組副本,用於歸併排序
	int foreIdx = mid;// 前半部分的指標
	int backIdx = end;// 後半部分的指標
	int counts = 0;// 記錄本次逆序對數
	int idxCopy = end;// 輔助數組的下標
	while(foreIdx>=start && backIdx >= mid+1) {
		if(data[foreIdx] > data[backIdx]) {
			copy[idxCopy--] = data[foreIdx--];
			counts += backIdx - mid;
		} else {
			copy[idxCopy--] = data[backIdx--];
		}
	}
	while(foreIdx >= start) {
		copy[idxCopy--] = data[foreIdx--];
	}
	while(backIdx >= mid+1) {
		copy[idxCopy--] = data[backIdx--];
	}
	for(int i=start; i<=end; i++) {
		data[i] = copy[i];
	}
	return (leftCounts+rightCounts+counts);
}  


36、滑動窗口最大值
答:題目:給定一個數組和滑動窗口的大小,找出所有滑動窗口裏數值的最大值。例如,如果輸入數組{2,3,4,2,6,2,5,1}及滑動窗口的大小3,那麼一共存在6個滑動窗口,他們的最大值分別爲{4,4,6,6,6,5}; 針對數組{2,3,4,2,6,2,5,1}的滑動窗口有以下6個: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
思路:首先暴力法是可以的,但是時間複雜度爲O(n*k)。可以使用雙端隊列作爲輔助結構,讓時間複雜度降爲O(n);即:隊列中存儲窗口大小的單調降序元素下標,隊首是當前窗口中的最大元素下標,後面則是最大元素後面的較大元素下標。
代碼如下:
vector<int> maxInWindows(const vector<int>& a, int k){
    vector<int> res;
    deque<int> d;
    for(int i = 0; i < a.size(); ++i){
        while(d.size()>0 && a[d.back()] <= a[i]) 
			d.pop_back();
        if(d.size()>0 && i - d.front() + 1 > k) 
			d.pop_front();
        d.push_back(i);

        if(k>0 && i+1 >= k)
			res.push_back(a[d.front()]);
    }
    return res;
}
遍歷過程中就兩個動作:
1)當前元素是否大於隊列尾部的下標表示的元素,若是則彈出尾部元素下標,然後繼續判斷;否則就將當前元素壓入隊列末端。
2)判斷隊列首部元素是否過期,若是則彈出首部元素;
3)上述過程後,隊列首部下標表示的元素就是當前窗口最大值的下標。

37、數據流中的中位數

答:題目:如何得到一個數據流中的中位數?如果從數據流中讀出奇數個數值,那麼中位數就是所有數值排序之後位於中間的數值。如果從數據流中讀出偶數個數值,那麼中位數就是所有數值排序之後中間兩個數的平均值。
思路:要求每讀入一個數據,求出當前已讀出序列的中位數。可以使用一個大頂堆和一個小頂堆(用優先隊列實現)結合起來存儲,大頂堆存儲前半部分較小序列,小頂堆存儲後半部分較大的序列。每讀取一個新數據,根據實際情況存入大根堆或者小根堆。然後中位數就是兩個堆頂數據之一或者兩個數平均值。
代碼如下:
priority_queue<int> qmax;
priority_queue<int,vector<int>,greater<int>> qmin;
void Insert(int num){
	if(qmin.size()>0 && num>qmin.top())
		qmin.push(num);
	else
		qmax.push(num);
	if(qmax.size()>= qmin.size()+2){
		qmin.push(qmax.top());
		qmax.pop();
	}else if(qmin.size()>= qmax.size()+2){
		qmax.push(qmin.top());
		qmin.pop();
	}
}

double GetMedian(){
	if(qmax.size()==qmin.size()) 
		return (qmax.top()+qmin.top())/2.0;
	else 
		return qmax.size()>qmin.size()? qmax.top():qmin.top();
}


38、快速冪

答:當求一個整型數的N次方時,若N特別大,那麼用循環連乘法就會時間複雜度非常大。這種情況一般用快速冪解決,公式:
          ①res=x^n=(x*x)^n/2;n爲偶數
          ②res=x^n=x*[(x*x)^n/2];n爲奇數
這樣一直分解下去,就可以使得時間複雜度變爲logn。實現方法是遞歸調用,代碼如下:
double Power(double base, int exponent) {
	if(exponent==0)
		return 1;
	if(base==0)
		return 0;
	double result=1;
	if(exponent>0){
		result=Power(base*base,exponent/2);//每一次遞歸調用函數,base的值就變爲上一層base的2次方
		if(exponent%2!=0)
			result=result*base;
	}
	else if(exponent<0){//考慮正負次冪,將負數次冪變爲正數次冪計算
		base=1/base;
		exponent=-1*exponent;
		result=Power(base,exponent);
	}

	return result;
}
假設exponent=33,這樣每次遞歸調用函數的結果如下:

39、中興筆試——加密方法

答:題目:已知s、n、m,根據下面公式求加密後的數值:
Res=[(x^n)%10]^m%1000000007;s爲大於0的正整數,n,m分別爲無符號整型(n,m可能非常大)
思路:(1)求冪次方需要使用上述小節中的“快速冪”方法,否則可能時間複雜度過大:
          ①res=x^n=(x*x)^n/2;n爲偶數
          ②res=x^n=x*[(x*x)^n/2];n爲奇數
(2)爲了防止數據不斷求冪次方發生數據溢出,冪次方求餘則需要使用如下公式:(x^n)%p=(x%p)^n%p;
(3)結合兩者的公式即可得出:
         ①res=x^n%p=(x*x)^n/2%p=[(x*x)%p]^n/2%p;n爲偶數
         ②res=x^n%p=x*(x*x)^n/2%p=x*{ [(x*x)%p]^n/2%p}%p;n爲奇數
所以,基本運算變爲[(x*x)%p]^n/2%p,若是奇數還需要對該結果乘以x並對p再求餘。
(4)代碼如下:用遞歸調用,先求y=x^n%10,再求y^m%1000000007;
unsigned long long GetBigMod(int x,int n,int mod){
	if(n==0)
		return 1;
	int p=mod;
    unsigned long long tmp=GetBigMod(x*x%p,n/2,p);
	if(n%2!=0)
		tmp=tmp*x%p;
    return tmp; 
}

int GetJiami(int x,int n,int m){
	int p1=10,p2=1000000007;
	return GetBigMod(GetBigMod(x,n,p1),m,p2);
}




發佈了38 篇原創文章 · 獲贊 202 · 訪問量 42萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章