posts - 34,comments - 2,trackbacks - 0
     摘要: 为什么要使用线程池:
创建多线程应用程序是非常困难的。需要会面临两个大问题。
一个是要对线程的创建和撤消进行管理,另一个是要对线程对资源的访问实施同步 。 
  阅读全文
posted @ 2011-10-10 09:25 Yu_ 阅读(804) | 评论 (0)编辑 收藏
     摘要: 若干种内核对象,包括进程,线程和作业。可以将所有这些内核对象用于同步目的。对于线程同步来说,这些内核对象中的每种对象都可以说是处于已通知或未通知的状态之中。

例如::当进程正在运行的时候,进程内核对象处于未通知状态,当进程终止运行的时候,它就变为已通知状态。进程内核对象中是个布尔值,当对象创建时,该值被初始化为FALSE(未通知状态)。当进程终止运行时,操作系统自动将对应的对象布尔值改为TRUE,表示该对象已经得到通知。当线程终止运行时,操作系统会自动将线程对象的状态改为已通知状态。因此,可以将相同的方法用于应用程序,以确定线程是否不再运行。
  阅读全文
posted @ 2011-10-08 00:10 Yu_ 阅读(376) | 评论 (0)编辑 收藏
     摘要: 线程需要在下面两种情况下互相进行通信:
• 当有多个线程访问共享资源而不使资源被破坏时。
• 当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。
  阅读全文
posted @ 2011-10-07 23:58 Yu_ 阅读(393) | 评论 (0)编辑 收藏
     摘要: 1、线程的组成
(1)、一个是线程的内核对象,操作系统用它管理线程。内核对象还是系统用来存放线程统计信息的地方。
(2)、一个线程堆栈,用于维护线程执行时所需的所有函数参数和局部变量。
  阅读全文
posted @ 2011-10-07 23:10 Yu_ 阅读(236) | 评论 (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_ 阅读(1338) | 评论 (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_ 阅读(386) | 评论 (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_ 阅读(756) | 评论 (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.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)关键字。

  通常在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_ 阅读(2561) | 评论 (1)编辑 收藏
     摘要: 1、平衡二叉树它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 如图:2、动态平衡技术 动态平衡技术Adelson-Velskii 和 Landis 提出了一个动态地保持二叉排序树平衡的方法,其基本思想是:  在构造二叉排序树的过程中,每当插入一个结点时,首先检查是否因插入而破坏了树的平衡性,如果是因插入结点而破坏了树的平衡性,则找出其中最小不平衡子树,...  阅读全文
posted @ 2011-10-04 01:09 Yu_ 阅读(731) | 评论 (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_ 阅读(1044) | 评论 (0)编辑 收藏
仅列出标题
共4页: 1 2 3 4