摘要: 為什么要使用線程池:
創建多線程應用程序是非常困難的。需要會面臨兩個大問題。
一個是要對線程的創建和撤消進行管理,另一個是要對線程對資源的訪問實施同步 ?!?
閱讀全文
posted @
2011-10-10 09:25 Yu_ 閱讀(864) |
評論 (0) |
編輯 收藏
摘要: 若干種內核對象,包括進程,線程和作業。可以將所有這些內核對象用于同步目的。對于線程同步來說,這些內核對象中的每種對象都可以說是處于已通知或未通知的狀態之中。
例如::當進程正在運行的時候,進程內核對象處于未通知狀態,當進程終止運行的時候,它就變為已通知狀態。進程內核對象中是個布爾值,當對象創建時,該值被初始化為FALSE(未通知狀態)。當進程終止運行時,操作系統自動將對應的對象布爾值改為TRUE,表示該對象已經得到通知。當線程終止運行時,操作系統會自動將線程對象的狀態改為已通知狀態。因此,可以將相同的方法用于應用程序,以確定線程是否不再運行。
閱讀全文
posted @
2011-10-08 00:10 Yu_ 閱讀(417) |
評論 (0) |
編輯 收藏
摘要: 線程需要在下面兩種情況下互相進行通信:
? 當有多個線程訪問共享資源而不使資源被破壞時。
? 當一個線程需要將某個任務已經完成的情況通知另外一個或多個線程時。
閱讀全文
posted @
2011-10-07 23:58 Yu_ 閱讀(433) |
評論 (0) |
編輯 收藏
摘要: 1、線程的組成
(1)、一個是線程的內核對象,操作系統用它管理線程。內核對象還是系統用來存放線程統計信息的地方。
(2)、一個線程堆棧,用于維護線程執行時所需的所有函數參數和局部變量。
閱讀全文
posted @
2011-10-07 23:10 Yu_ 閱讀(270) |
評論 (0) |
編輯 收藏
討論三個問題:
1、進程間如何通信呢,如何來相互傳遞信息呢?
(1)、低級通信:只能傳遞狀態和整數值(控制信息)
–信號量(semaphore)
–信號(signal)
(2)、高級通信:能夠傳送任意數量的數據
–共享內存(shared memory)
–消息傳遞(message passing)
–管道(pipe)
剪貼板:
基本機制是:系統預留的一塊全局共享內存,可用于被各進程暫時存儲數據。寫入進程首先創建一個全局內存塊,并將數據寫到該內存塊;接受數據的進程通過剪貼板機制獲取此內存塊的句柄,并完成對該內存塊數據的讀取。
管道包括三種:
管道(Pipe)實際是用于進程間通信的一段共享內存,創建管道的進程稱為管道服務器,連接到一個管道的進程為管道客戶機。一個進程在向管道寫入數據后,另一進程就可以從管道的另一端將其讀取出來。匿名管道(Anonymous Pipes)是在父進程和子進程間單向傳輸數據的一種未命名的管道,只能在本地計算機中使用,而不可用于網絡間的通信。
1)
普通管道PIPE, 通常有種限制,一是半雙工,只能
單向傳輸; 二是只能在
父子或者兄弟進程間使用.
2)
流管道s_pipe: 去除了第一種限制,可以
雙向傳輸.
3)
命名管道:name_pipe, 去除了第二種限制,
可以在許多并不相關的進程之間進行通訊.
郵件槽:
郵件槽(Mailslots)提供進程間
單向通信能力,任何進程都能建立郵件槽成為郵件槽服務器。其它進程,稱為郵件槽客戶,可以通過郵件槽的名字給郵件槽服務器進程發送消息。進來的消息一直放在郵件槽中,直到服務器進程讀取它為止。一個進程既可以是郵件槽服務器也可以是郵件槽客戶,因此可
建立多個郵件槽實現進程間的雙向通信。 通過郵件槽可以給本地計算機上的郵件槽、其它計算機上的郵件槽或指定網絡區域中所有計算機上有同樣名字的郵件槽發送消息。廣播通信的消息長度不能超過400字節,非廣播消息的長度則受郵件槽服務器指定的最大消息長度的限制。
郵件槽與命名管道相似,不過它傳輸數據是
通過不可靠的數據報(如TCP/IP協議中的UDP包)完成的,一旦網絡發生錯誤則無法保證消息正確地接收,而命名管道傳輸數據則是建立在可靠連接基礎上的。不過郵件槽有簡化的編程接口和給指定網絡區域內的所有計算機廣播消息的能力,所以郵件槽不失為應用程序發送和接收消息的另一種選擇。
優缺點:
郵槽最大的一個缺點便是只允許從客戶機到服務器,建立一種不可靠的單向數據通信。
而另一方面,郵槽最大的一個優點在于,它們使客戶機應用能夠非常容易地將廣播消息發送給一個或多個服務器應用。
共享內存:
存在于內核級別的一種資源,共享內存指在多處理器的計算機系統中,可以被不同中央處理器(CPU)訪問的大容量內存。由于多個CPU需要快速訪問存儲器,這樣就要對存儲器進行緩存(Cache)。任何一個緩存的數據被更新后,由于其他處理器也可能要存取,共享內存就需要立即更新,否則不同的處理器可能用到不同的數據。共享內存 (shared memory)是 Unix下的多進程之間的通信方法 ,這種方法通常用于一個程序的多進程間通信,實際上多個程序間也可以通過共享內存來傳遞信息。
2、當兩個或者多個進程訪問共享資源時,如何確保他們不會相互妨礙-----進程互斥問題。
原因:
進程宏觀上并發執行,依靠時鐘中斷來實現微觀上輪流執行。當兩個或者多個進程對同一個共享內存訪問,結果不能預測。在同一時刻,只允許一個進程訪問該共享數據,即如果當前已有一個進程正在使用該數據,那么其他進程暫時不能訪問。這就是互斥的概念。
實現互斥訪問的四個條件:
(1)、任何兩個進程都不能同時進入臨界區;
(2)、不能事先假定CPU的個數和運行速度;
(3)、當一個進程運行在它的臨界區外面時,不能妨礙其他的進程進入臨界區;
(4)、任何一個進程進入臨界區的要求應該在有限時間內得到滿足。
(解決辦法)
(1)、用標志位加鎖。
lock的初始值為0,當一個進程想進入臨界區時,先查看lock的值,若為1,說明已有進程在臨界區內,只好循環等待。等它變成了0,才可進入。

缺點是:lock也是一個共享資源,當進程競爭lock時,可能會出現問題。加鎖標志位法的缺點在于可能出現針對共享變量 lock 的競爭狀態。例如,當進程 0 執行完循環判斷語句后,被時鐘中斷打斷,從而可能使多個進程同時進入臨界區。
是一種不安全的做法、
(2)、強制輪流法
基本思想:每個進程嚴格地按照輪流的順序來進入臨界區。
優點:保證在任何時刻最多只有一個進程在臨界區
缺點:違反了互斥訪問四條件中的第三個條件,當一個進程運行在它的臨界區外面時,不能妨礙其他的進程進入臨界區

(3)、Peterson方法。
當一個進程想進入臨界區時,先調用enter_region函數,判斷是否能安全進入,不能的話等待;當它從臨界區退出后,需調用leave_region函數,允許其它進程進入臨界區。兩個函數的參數均為進程號。

小結:
當一個進程想要進入它的臨界區時,首先檢查一下是否允許它進入,若允許,就直接進入了;若不允許,就在那里循環地等待,一直等到允許它進入。
缺點:
1)浪費CPU時間;
2)可能導致預料之外的結果(如:一個低優先級進程位于臨界區中,這時有一個高優先級的進程也試圖進入臨界區)
3、當進程間存在某種依存關系時,如何來調整他們運行的先后次序-----進程同步問題。
用P,V原語操作實現同步(略)
另外:上述的問題也適合線程嗎??
posted @
2011-10-07 15:44 Yu_ 閱讀(1395) |
評論 (0) |
編輯 收藏
1、什么是進程?
::一般將進程定義成一個正在運行的程序的一個實例。進程由兩部分組成:
①、一個內核對象,操作系統用它來管理進程。內核對象也是系統保存進程統計信息的地方。
②、一個地址空間,其中包含所有執行體(executable)或DLL模塊的代碼和數據。此外,它還包含動態內存分配,比如線程堆棧和堆的分配。
進程與線程的關系:
①、一個進程創建的時候,系統會自動創建它的第一個線程,這稱為主線程(primary thread)。
②、進程要做任何事情,都必須讓一個線程在它的上下文中運行。如果沒有線程要執行進程地址空間包含的代碼,進程就失去了繼續存在的理由。所以,系統會自動銷毀進程及其地址空間。
③、一個進程可以有多個線程,所有線程都在進程的地址空間中“同時”執行代碼。為此,每個線程都有它自己的一組CPU寄存器和它自己的堆棧。對于所有要運行的線程,操作系統會輪流為每個線程調度一些CPU時間。它會采取round-robin(輪詢或輪流)方式,為每個線程都分配時間片,從而營造出所有線程都在“并發”運行的假象。
2、系統如何創建一個進程內核對象來管理每個進程。
當一個進程被初始化時,系統要為它分配一個句柄表(空的,也就是用來管理進程的內核對象)。該句柄表只用于內核對象(而不用于用戶對象和gdi對象)。句柄表是一個數據結構的數組,每個結構都包含一個指向內核對象的指針,一個 訪問屏蔽(DWORD)和一個標志(DWORD)。
:::當進程中的線程調用創建內核對象的函數(比如 CreatFileMapping)時,內核就為該對象分配一個內存塊并對它初始化。同時對進程的句柄表進行掃描,找出一個空項,填充內核對象數據結構的內存地址到該頂的指針成員,設置訪問屏蔽和標志。
::: 用于創建內核對象的所有函數均返回與進程相關的句柄。該句柄實際上是放入進程的句柄表中的索引 (由此可知,句柄是與進程相關的,不能由其他進程直接成功地使用)。但這只適用部分系統,句柄的含義可能隨時變更。 應用程序在運行時有可能泄漏內核對象,但是當進程終止時系統將能確保所有內容均被正確地清除。這個情況也適用于所有對象,資源和內存塊,也就是說當進程終止運行時,系統將保證進程不會留 下任何對象。
3、如何利用與一個進程關聯的內核對象來操縱該進程。
4、進程的各種不同的屬性(或特性),以及用于查詢和更改這些屬性的幾個函數。
實例句柄、前一個實例句柄、進程的命令行、進程的環境變量、進程當前所在的驅動器和目錄、還有版本問題等
5、如何利用一些函數在系統中創建或生成額外的進程。
我們用CreateProcess函數來創建一個進程,參考MSDN。當一個線程調用CreateProcess時,系統會做如下工作:
(1)、系統將創建一個進程內核對象,其初始使用計數為1。進程內核對象不是進程本身,而是操作系統用來管理這個進程的一個小型數據結構(該內核對象是用來管理新進程的)。
(2)、系統為新進程創建一個虛擬地址空間,并將執行體文件(和所有必要的DLL)的代碼及數據加載到進程的地址空間。
(3)、系統為新進程的主線程創建一個線程內核對象(使用計數為1)。和進程內核對象一樣,線程內核對象也是一個小型數據結構,操作系統用它來管理這個線程。這個主線程一開始就會執行由鏈接器設為應用程序入口的C/C++運行時啟動例程,并最終調用你的WinMain,wWinMain,main或wmain函數。
(4)、如果系統成功創建了新進程和主線程,CreateProcess將返回TRUE。
創建子進程后:
創建一個進程內核對象時,系統會為此對象分配一個獨一無二的標識符,系統中沒有別的進程內核對象會有相同的ID編號
6、如何終止線程。
關閉到一個進程或線程的句柄,不會強迫系統殺死此進程或線程。關閉句柄只是告訴系統你對進程或線程的統計數據
不再感興趣了。進程或線程會繼續執行,直至自行終止。(計數,重點是計數)
進程可以通過以下4種方式終止:
(1)、主線程的入口函數返回(強烈推薦的方式)。
讓主線程的入口函數返回,可以保證發生以下幾件事情:
?? 該線程創建的任何C++對象都將由這些對象的析構函數正確銷毀。
?? 操作系統將正確釋放線程堆棧使用的內存。
?? 系統將進程的退出代碼(在進程內核對象中維護)設為你的入口函數的返回值。
?? 系統遞減進程內核對象的使用計數。
(2)、進程中的一個線程調用ExitProcess函數(要避免這個方式)
進程會在該進程中的一個線程調用ExitProcess函數時終止:
VOID ExitProcess(UINT fuExitCode);
一旦你的應用程序的主線程從它的入口函數返回,那么不管當前在進程中是否正在運行其他線程,都會調用ExitProcess來終止進程。不過,如果在入口函數中調用ExitThread,而不是調用ExitProcess或者簡單地返回,應用程序的主線程將停止執行,但只要進程中還有其他線程正在運行,進程就不會終止。
(3)、另一個進程中的線程調用TerminateProcess函數(要避免這個方式)
調用TerminateProcess也可以終止一個進程,但是進程無法將它在內存中的任何信息轉儲到磁盤上。
(4)、進程中的所有線程都“自然死亡”(這是很難發生的)
posted @
2011-10-07 11:19 Yu_ 閱讀(427) |
評論 (0) |
編輯 收藏
1、什么是內核對象?
內核對象的數據結構只能由內核訪問。
他們有:令牌(access token)對象、事件對象、文件對象、文件映射對象、I/O完成端口對象、作業對象、mailslot對象、mutex對象、pipe對象、進程對象、semaphore對象、線程對象、waitable timer對象以及thread pool worker factory對象等等。大多數成員都是不同的對象類型特有的。
2、生命周期。
取決與其計數:
內核對象的所有者是內核,而非進程。換言之,如果你的進程調用一個函數來創建了一個內核對象,然后進程終止運行,則內核對象并不一定會銷毀。大多數情況下,這個內核對象是會銷毀的,但假如另一個進程正在使用你的進程創建的內核對象,那么在其他進程停止使用它之前,它是不會銷毀的。總之,內核對象的生命期可能長于創建它的那個進程。
內核知道當前有多少個進程正在使用一個特定的內核對象,因為每個對象都包含一個使用計數(usage count)。
使用計數是所有內核對象類型都有的一個數據成員。初次創建一個對象的時候,其使用計數被設為1。另一個進程獲得對現有內核對象的訪問后,使用計數就會遞增。進程終止運行后,內核將自動遞減此進程仍然打開的所有內核對象的使用計數。一個對象的使用計數變成0,內核就會銷毀該對象。這樣一來,可以保證系統中不存在沒有被任何進程引用的內核對象。
3、管理內核對象
一個進程在初始化時,系統將為它分配一個句柄表。一個進程的句柄表,它只是一個由數據結構組成的數組。每個結構
都包含指向一個內核對象的指針、一個訪問掩碼(access mask)和一些標志。
(1)、創建一個內核對象
一個進程首次初始化的時候,其句柄表為空。當進程內的一個線程調用一個會創建內核對象的函數時,內核將為這個對象分配并初始化一個內存塊。然后,內核掃描進程的句柄表,查找一個空白的記錄項(empty entry)。指針成員會被設置成內核對象的數據結構的內部內存地址,訪問掩碼將被設置成擁有完全訪問權限,標志也會設置。
如果創建調用失敗,那么返回的句柄值通常為0(NULL)。之所以失敗,可能是由于系統內存不足,或者遇到了一個安全問題。遺憾的是,有幾個函數在調用失敗時會返回句柄值–1。所以要看清楚再判斷。
(2)、關閉內存對象
無論以什么方式創建內核對象,都要調用CloseHandle向系統指出你已經結束使用對象:
BOOL CloseHandle(HANDLE hobject);
結束時需要注意::;
①、在內部,該函數首先檢查主調進程的句柄表,驗證“傳給函數的句柄值”標識的是“進程確實有權訪問的一個對象”。如果句柄是有效的,系統就將獲得內核對象的數據結構的地址,并在結構中遞減“使用計數”成員。如果使用計數變成0,內核對象將被銷毀,并從內存中刪除。
②、一旦調用CloseHandle,你的進程就不能訪問那個內核對象。
③、如果對象的使用計數沒有遞減至0,它就不會被銷毀。它表明另外還有一個或多個進程在使用該對象。當其他進程全部停止使用這個對象后(通過調用CloseHandle),對象就會被銷毀。
④、在創建一個內核對象時,我們會將它的句柄保存到一個變量中。將此變量作為參數調用了CloseHandle函數后,還應同時將這個變量設為NULL。
⑤、當進程終止運行,操作系統會確保此進程所使用的所有資源都被釋放!對于內核對象,操作系統執行的是以下操作:進程終止時,系統自動掃描該進程的句柄表。如果這個表中有任何有效的記錄項(即進程終止前沒有關閉的對象),操作系統會為你關閉這些對象句柄。任何這些對象的使用計數遞減至0,內核就會銷毀對象。
(3)、進程共享內核對象、
很多地方需要用到進程共享內核對象。例如
①、利用文件映射對象,可以在同一臺機器上運行的兩個不同進程之間共享數據塊。
②、借助mailslots(信箱)和named pipes(匿名管道),在網絡中的不同計算機上運行的進程可以相互發送數據塊。
③、mutexes(互斥對象)、semaphores(信標對象)和事件允許不同進程中的線程同步執行。例如,一個應用程序可能需要在完成某個任務之后,向另一個應用程序發出通知。
三種不同的機制來允許進程共享內核對象:使用對象句柄繼承;為對象命名;以及復制對象句柄。
1 使用對象句柄繼承
設置可繼承標志為1。對象句柄的繼承只會在生成子進程的時候發生。
程序初始化時會為父進程創建一個進程句柄表,使用對象句柄繼承,系統會為子進程創建一個新的、空白的進程句柄表——就像它為任何一個新進程所做的那樣。系統會遍歷父進程的句柄表,對它的每一個記錄項進行檢查。凡是包含一個有效的“可繼承的句柄”的項,都會被完整地拷貝到子進程的句柄表。在子進程的句柄表中,拷貝項的位置與它在父進程句柄表中的位置是完全一樣的。這是非常重要的一個設計,因為它意味著:在父進程和子進程中,對一個內核對象進行標識的句柄值是完全一樣的。內核對象的使用計數將遞增。
2、改變句柄的標志
父進程創建了一個內核對象,得到了一個可繼承的句柄,然后生成了兩個子進程。但是,父進程只希望其中的一個子進程繼承內核對象句柄。調用SetHandleInformation函數來改變內核對象句柄的繼承標志。函數具體參考MSDN。
3、為對象命名
不能創建相同名字的對象,類型不同也不行。進程間共享:
進程A 有某個命名"JeffMutex" 對象 hMutexProcessA,那么進程B 創建一個同樣"JeffMutex" 對象時。,
系統首先會查看是否存在一個名為“JeffMutex”的內核對象。由于確實存在這樣的一個對象,所以內核接著檢查對象的類型。由于試圖創建一個mutex,而名為“JeffMutex”的對象也是一個mutex,所以系統接著執行一次安全檢查,驗證調用者是否擁有對象的完全訪問權限。如果答案是肯定的,系統就會在Process B的句柄表中查找一個空白記錄項,并將其初始化為指向現有的內核對象。如果對象的類型不匹配,或調用者被拒絕訪問,CreateMutex就會失敗(返回NULL)。
這樣就實現了進程共享對象。但問題是進程B不知道自己創建的是新對象,還是用久對象。
(4)、復制對象句柄
為了跨越進程邊界來共享內核對象,最后一個技術是使用DuplicateHandle函數。
這個函數獲得一個進程的句柄表中的一個記錄項,然后在另一個進程的句柄表中創建這個記錄項的一個拷貝。
posted @
2011-10-06 17:27 Yu_ 閱讀(799) |
評論 (0) |
編輯 收藏
1、B樹的定義
B樹是一種平衡的多分樹(m叉樹),通常我們說m階的B樹,它必須滿足如下條件:
(1)每個結點至多有m個子結點;
(2)若根結點不是葉子結點,則至少有兩棵子樹;
(3)所有的葉結點在同一層;
(4)有k個子結點的非根結點恰好包含k-1個關鍵碼。
2、B-樹數據結構
#define M 4 //B-樹的階,暫設為4
#define false 0
#define true 1
typedef struct BTNode
{
int keynum; //節點中關鍵字個數,即節點的大小
struct BTNode *parent; //指向雙親結點
int key[M+1]; //關鍵字向量,0號單元未用
struct BTNode *son[M+1]; //子樹指針向量
//Record *recptr[M+1]; //記錄指針向量,0號單元未用(文件中使用)
}BTNode, *BTree; //B-樹節點和B-樹的類型
typedef struct
{
BTNode *pt; //指向找到的節點
int pos; //1...m,在節點中的關鍵字序號
int tag; //1:查找成功,0:查找失敗
}Result; //B-樹的查找結果類型
//初始化
void init_BTree(BTree &root)
{
root=NULL;
}
2、B樹的查找
B樹上的查找是一個順指針查找結點和在結點內的關鍵碼中查找交叉進行的過程。從根結點開始,在結點包含的關鍵碼中查找給定的關鍵碼,找到則查找成功;否則確定給定關鍵碼可能在的子樹,重復上面的操作,直到查找成功或者指針為空為止。
下圖顯示了在B樹中查找關鍵碼21的過程。
int search(BTree &p,int key)
{
int j;
for(j=1; j<=p->keynum; j++)
if(p->key[j] > key)
{
break;
}
return j-1; //應該插入的位置的前一位
}
Result searchBtree(BTree &root, int key)
{
//在m階B樹t上查找關鍵碼key,反回(pt,i,tag)。
//若查找成功,則特征值tag=1,指針pt所指結點中第i個關鍵碼等于key;
//否則,特征值tag=0,等于key的關鍵碼記錄,應插入在指針pt所指結點中第i個和第i+1個關鍵碼之間
int found=false;
int i;
BTree p=root,father=NULL; //初始化,p指向待查節點,q指向p的雙親
Result result; //SearchBTree函數返回值
while(p && !found)
{
i=search(p,key); //p->node[i].key≤K<p->node[i+1].key
if(i>0 && p->key[i]==key)
{
found=true; //找到待查關鍵字
}
else
{
father=p;
p=p->son[i];
}
}
result.pos=i+1; //pos是插入的位置,記住加1
if(found) //查找成功
{
result.pt=p;
result.tag=1;
}
else //查找不成功,返回key的插入位置i
{
result.pt=father;
result.tag=0;
}
return result;
}//SearchBTree
3、B樹的插入
首先是在恰當的葉子結點中添加關鍵碼,如果該結點中關鍵碼不超過m-1個,則插入成功。否則要把這個結點分裂為兩個。并把中間的一個關鍵碼拿出來插到結點的父結點里去。父結點也可能是滿的,就需要再分裂,再往上插。最壞的情況,這個過程可能一直傳到根,如果需要分裂根,由于根是沒有父結點的,這時就建立一個新的根結點。插入可能導致B樹朝著根的方向生長。
B-樹的生成從空樹開始,逐個插入關鍵字而得。關鍵字的個數必須至少為[m/2]-1,每次插入總在最底層某個終端結點添加一個關鍵字,如果該結點關鍵字個數小于m-1則直接插入,如果發現新插入關鍵字后,關鍵字總數超過m-1個則結點需要分裂,做法如下:
(a)假設結點p中已經含有m-1個關鍵字,再插入一個關鍵字之后(插入總要保持關鍵字數組的大小有序,從小到大排好序),可以將p分裂為p和p’,其中p含有的信息為[m/2]-1([m]表示大于m的最小整數),p’含有的信息為m-[m/2] ([m]表示大于m的最小整數)。然后將關鍵字K[m/2]和指向p’的指針則一起插入到p的雙親結點中去。
(b)檢查雙親結點,如果雙親結點出現(a)的情況,則回到步驟a繼續執行。直到插入滿足條件為止,樹的深度增加過程是隨著插入而自下而上生長的過程。
下圖顯示了在B樹中插入關鍵碼33的過程。

void split(BTree &q, int s, BTree &ap)
{
// 將結點q分裂成兩個結點,前一半保留,后一半移入新生結點ap
int i;
cout<<"分裂!"<<" "<<q->key[s]<<endl;
ap=(BTree)malloc(sizeof(BTNode)); //生成新結點ap
ap->son[0] = q->son[s]; //原來結點中間位置關鍵字相應指針指向的子樹放到新生成結點的0棵子樹中去
for(i=s+1;i<=M;i++) //后一半移入ap
{
ap->key[i-s]=q->key[i];
ap->son[i-s]=q->son[i];
}//for
ap->keynum=M-s;
ap->parent=q->parent;
q->keynum=s-1; //q的前一半保留,修改keynum
}//split
void NewRoot(BTree &root, int x, BTree &ap) //生成新的根節點
{
//生成含信息(root,r,ap)的新的根結點*root,原root和ap為子樹指針
BTree p;
p=(BTree)malloc(sizeof(BTNode));
if(root) //如果原來的樹不是空樹
root->parent=p; //遠來的根的雙親指針指向新根
p->son[0]=root; //新根的第一個孩子節點是原來的根節點
root=p; //root指向新根
root->parent=NULL; //新根的雙親是空指針
root->keynum=1;
root->key[1]=x; //新根的第一個關鍵字就是前面分裂出來的關鍵字
root->son[1]=ap; //新根的第二個孩子節點是原來的根中分裂出來的節點
if(ap) //如果原來的樹不是空樹
ap->parent=root; //原來的根中分裂出來的節點的雙親指針指向新根
}//NewRoot
void insert(BTree &q, int i, int key, BTree &ap) //插入
{
int j;
for(j=q->keynum; j>=i; j--)
{
q->key[j+1]=q->key[j];
}
q->key[i]=key;
for(j=q->keynum; j>=i; j--)
{
q->son[j+1]=q->son[j];
}
q->son[i]=ap;
q->keynum++;
}//insert
void insertBtree(BTree &root, int key, BTree &q, int i)
{
//在B-樹T上節點q的key[i]和key[i+1]之間插入關鍵字key
//若引起節點過大,則沿雙親鏈進行必要的節點分裂整理,使T仍是M階的B-樹
BTree ap=NULL;
int x=key;
int finished = false;
int s;
while(q && !finished)
{
insert(q, i, x, ap); //將key和ap分別插入到q->key[i+1]和q->son[i+1]
if(q->keynum < M)
finished = true; //插入完成
else
{ //分裂結點*q
s=ceil(M/2);
x=q->key[s];
split(q, s, ap); //將q->key[s+1...M],q->son[s...M]和q->recptr[s+1...M]移入到新節點*ap
q=q->parent;
if(q)
i=search(q,x)+1; //在雙親結點*q中去查找x的插入位置,記住加1,因為search()返回的是插入位置的前一位
}//else
}//while
if(!finished) //root是空樹(參數q初值為NULL)或者根節點已分裂為節點*q和*ap
NewRoot(root, x, ap); //生成含信息(root,x,ap)的新的根節點*root,原root和ap為子樹指針
}//insertBtree
void SearchInsertBTree(BTree &root,int key)//搜索插入
{
//在m階B樹*t上結點*q的key[i],key[i+1]之間插入關鍵碼key
//若引起結點過大,則沿雙親鏈進行必要的結點分裂調整,使*t仍為m階B樹
Result rs;
rs = searchBtree(root,key);
if(!rs.tag) //tag=0查找不成功,插入
{
cout<<"樹中沒有相同的節點,插入!"<<endl;
insertBtree(root, key, rs.pt, rs.pos); //在B-樹T上節點re.pt的key[i]和key[i+1]之間插入關鍵字key
}
else
{
cout<<"樹中已有相同的節點!"<<endl;
}
}//InserBTree
4、B樹的刪除
B樹中的刪除操作與插入操作類似,但要稍微復雜些。如果刪除的關鍵碼不在葉結點層,則先把此關鍵碼與它在B樹里的后繼對換位置,然后再刪除該關鍵碼。如果刪除的關鍵碼在葉結點層,則把它從它所在的結點里去掉,這可能導致此結點所包含的關鍵碼的個數小于 -1。這種情況下,考察該結點的左或右兄弟,從兄弟結點移若干個關鍵碼到該結點中來(這也涉及到它們的父結點中的一個關鍵碼要做相應變化),使兩個結點所含關鍵碼個數基本相同。只有在兄弟結點的關鍵碼個數也很少,剛好等于 -1時,這個移動不能進行。這種情況下,要把將刪除關鍵碼的結點,它的兄弟結點及它們的父結點中的一個關鍵碼合并為一個結點。
B+樹
B+樹是應文件系統所需而出的一種B-樹的變型樹。一棵m階的B+樹和m階的B-樹的差異在于:
1.有n棵子樹的結點中含有n個關鍵字。
2.所有的葉子結點中包含了全部關鍵字的信息,及指向含這些關鍵字記錄的指針,且葉子結點本身依關鍵字的大小自小而大順序鏈接。
3.所有的非終端結點可以看成是索引部分,結點中僅含其子樹(根結點)中的最大(或最?。╆P鍵字。
通常在B+樹上有兩個頭指針,一個指向根結點,一個指向關鍵字最小的葉子結點。
1、B+樹的查找
對B+樹可以進行兩種查找運算:
1.從最小關鍵字起順序查找;
2.從根結點開始,進行隨機查找。
在查找時,若非終端結點上的劇組機等于給定值,并不終止,而是繼續向下直到葉子結點。因此,在B+樹中,不管查找成功與否,每次查找都是走了一條從根到葉子結點的路徑。其余同B-樹的查找類似。
2、B+樹的插入
m階B樹的插入操作在葉子結點上進行,假設要插入關鍵值a,找到葉子結點后插入a,做如下算法判別:
①如果當前結點是根結點并且插入后結點關鍵字數目小于等于m,則算法結束;
②如果當前結點是非根結點并且插入后結點關鍵字數目小于等于m,則判斷若a是新索引值時轉步驟④后結束,若a不是新索引值則直接結束;
③如果插入后關鍵字數目大于m(階數),則結點先分裂成兩個結點X和Y,并且他們各自所含的關鍵字個數分別為:u=大于(m+1)/2的最小整數,v=小于(m+1)/2的最大整數;
由于索引值位于結點的最左端或者最右端,不妨假設索引值位于結點最右端,有如下操作:
如果當前分裂成的X和Y結點原來所屬的結點是根結點,則從X和Y中取出索引的關鍵字,將這兩個關鍵字組成新的根結點,并且這個根結點指向X和Y,算法結束;
如果當前分裂成的X和Y結點原來所屬的結點是非根結點,依據假設條件判斷,如果a成為Y的新索引值,則轉步驟④得到Y的雙親結點P,如果a不是Y結點的新索引值,則求出X和Y結點的雙親結點P;然后提取X結點中的新索引值a’,在P中插入關鍵字a’,從P開始,繼續進行插入算法;
④提取結點原來的索引值b,自頂向下,先判斷根是否含有b,是則需要先將b替換為a,然后從根結點開始,記錄結點地址P,判斷P的孩子是否含有索引值b而不含有索引值a,是則先將孩子結點中的b替換為a,然后將P的孩子的地址賦值給P,繼續搜索,直到發現P的孩子中已經含有a值時,停止搜索,返回地址P。
3、B+樹的刪除
B+樹的刪除也僅在葉子結點進行,當葉子結點中的最大關鍵字被刪除時,其在非終端結點中的值可以作為一個“分界關鍵字”存在。若因刪除而使結點中關鍵字的個數少于m/2 (m/2結果取上界,如5/2結果為3)時,其和兄弟結點的合并過程亦和B-樹類似。
另外的看法,當作補充和豐富吧。B樹,B-樹和B+樹是三個不同的概念。
B樹
二叉排序樹(Binary Sort Tree)又稱二叉查找樹,也叫B樹。
它或者是一棵空樹;或者是具有下列性質的二叉樹:
(1)若左子樹不空,則左子樹上所有結點的值均小于左子樹所在樹的根結點的值;
(2)若右子樹不空,則右子樹上所有結點的值均大于右子樹所在樹的根結點的值;
(3)左、右子樹也分別為二叉排序樹;
1、二叉排序樹(B樹)的查找:
時間復雜度與樹的深度的有關。
步驟:若根結點的關鍵字值等于查找的關鍵字,成功。
否則:若小于根結點的關鍵字值,遞歸查左子樹。
若大于根結點的關鍵字值,遞歸查右子樹。
若子樹為空,查找不成功。
2、二叉排序樹(B樹)的插入和刪除:
二叉排序樹是一種動態樹表。其特點是:樹的結構通常不是一次生成的,而是在查找過程中,當樹中不存在關鍵字等于給定值的節點時再進行插入。新插入的結點一定是一個新添加的葉子節點,并且是查找不成功時查找路徑上訪問的最后一個結點的左孩子或右孩子結點。
插入算法:
首先執行查找算法,找出被插結點的父親結點。
判斷被插結點是其父親結點的左兒子還是右兒子。將被插結點作為葉子結點插入。
若二叉樹為空。則首先單獨生成根結點。
注意:新插入的結點總是葉子結點,所以算法復雜度是O(h)。
刪除算法:
如果刪除的結點沒有孩子,則刪除后算法結束;
如果刪除的結點只有一個孩子,則刪除后該孩子取代被刪除結點的位置;
如果刪除的結點有兩個孩子,則選擇右孩子為根的樹,它的左子樹中,值最小的點作為新的根,同時在該最小值處開始,執行刪除算法,如此繼續到刪除算法的前兩種情況時,刪除算法結束。
B樹用途:查找信息快速,但是隨著查找深度的增加,會影響查找的效率,所以,通常會使用平衡二叉樹的平衡算法來進行動態平衡。
posted @
2011-10-05 19:09 Yu_ 閱讀(2614) |
評論 (1) |
編輯 收藏
摘要: 1、平衡二叉樹它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,并且左右兩個子樹都是一棵平衡二叉樹?!∪鐖D:2、動態平衡技術 動態平衡技術Adelson-Velskii 和 Landis 提出了一個動態地保持二叉排序樹平衡的方法,其基本思想是: 在構造二叉排序樹的過程中,每當插入一個結點時,首先檢查是否因插入而破壞了樹的平衡性,如果是因插入結點而破壞了樹的平衡性,則找出其中最小不平衡子樹,...
閱讀全文
posted @
2011-10-04 01:09 Yu_ 閱讀(774) |
評論 (0) |
編輯 收藏
1、順序查找:(有兩種方法)
在一個已知無(或有序)序隊列中找出與給定關鍵字相同的數的具體位置。
①、讓關鍵字與隊列中的值從
第一個開始逐個比較,直到找出與給定關鍵字相同的值為止。
int SeqSearch(Seqlist R,KeyType K)
{
//在順序表R[1..n]中順序查找關鍵字為K的結點,
//成功時返回找到的結點位置,失敗時返回-1
int i;
for(i=0;i<R.len;i++)
{
if(R[i].key==K) return i;
}
return -1;
} //SeqSearch②、從表中最后一個記錄開始,逐個進行與關鍵字比較。直至第一個值,elem[0]=key,為設置“哨兵”。找不到返回0
int SeqSearch(Seqlist R,KeyType K)
{
//在順序表R[1..n]中順序查找關鍵字為K的結點,
//成功時返回找到的結點位置,失敗時返回0
int i;
R[0].key=K; //設置哨兵
for(i=R.len;R[i].key!=K;i--); //從表后往前找
return i; //若i為0,表示查找失敗,否則R[i]是要找的結點
} //SeqSearch比較:
成功時的順序查找的平均查找長度:
在等概率情況下,pi=1/n(1≤i≤n),故成功的平均查找長度為
(n+…+2+1)/n=(n+1)/2
即查找成功時的平均比較次數約為表長的一半。
若K值不在表中,則須進行n+1次比較之后才能確定查找失敗。
2、折半查找:(二分法查找)(必須有序)
①、假設數據是按升序排序的,對于給定值x,從序列的中間位置開始比較,如果當前位置值等于x,則查找成功;
②、若x小于當前位置值,則在數列的前半段中查找;若x大于當前位置值則在數列的后半段中繼續查找,直到找到為止。
int search(int *a,int key,int low,int high)
{
int mid;
mid = (low + high)/2;
while(low<high)
{
if(a[mid] == key) return mid;
else
if (a[mid]>key) high=mid;
else low=mid;
mid = (low + high)/2;
}//while
return -1; //沒有找到
}//search
用遞歸思想:
int search(int *a,int key,int low,int high)
{
int mid;
if(low > high)
return -1;
mid = (low + high)/2;
if(a[mid] == key) return mid;
else if(a[mid] > key) return search(a,key,low,mid -1);
else return search(a,key,mid + 1,high);
}
3、
/////////////////////待續...
posted @
2011-10-03 11:00 Yu_ 閱讀(1093) |
評論 (0) |
編輯 收藏