`
kofsky
  • 浏览: 201586 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
社区版块
存档分类
最新评论

讨论记录之哈希表与二叉树

阅读更多

Participants: ZY, LF, HZP, CPP

Date: <st1:chsdate isrocdate="False" month="9" w:st="on" day="23" islunardate="False" year="2008">08-9-23</st1:chsdate> 7:20 PM<o:p></o:p>

Recorder: LF, CPP<o:p></o:p>

<o:p> </o:p>

参考文献

1、《算法导论》

2、《数据结构》严蔚敏 吴伟民 编著,清华大学出版社——经典的数据结构教材

<o:p> </o:p>

本次讨论的重点是 二叉排序树的插入删除(由此引入查找一般二叉树中结点的中序直接前驱和后继) 一般二叉树的非递归遍历,对Hash查找(有冲突时)的平均长度也进行了深入的分析。

哈希表(散列)

       在基于“比较”的一系列查找方法中,记录的关键字和记录的相对位置之间没有确定的关系,查找效率依赖于比较次数。而哈希表查找是利用记录的关键字与它的存储位置之间的关系f(即Hash函数) ,不需比较便可直接取得所查记录,显然,哈希表存取比较方便,但是存储时容易产生冲突(collision): 即不同的关键字可以对应统一个地址。因此,如何确定hash函数和解决冲突是哈希表查找的关键。

1.1   Hash函数的构造方法

直接定址法 H(k)=key H(k)=a*key+b(线形函数) 例如

                     人口数字统计表(年龄作为关键字,哈希函数取关键字自身)

                     地址       1     2     3     …………       100

                     年龄       1     2     3     …………       100

                     人数       67    35    33    …………       244 

数字分析法:取关键字的若干数位组成哈希地址

如:关键字如下:若哈希表长为100则可取中间两位十进制数作为哈希地址。

81346532       81372242       81387422       ……       81354157 (划横线部分为地址)

平方取中法:关键字平方后取中间几位数组成哈希地址

除留余数法:取关键字被某个不大于表长m的数p除后所得的余数为哈希地址。

                     H(k) = k mod p      p<=m

折叠法,随机数法

1.2   处理冲突的方法

         假设地址集为0..n-1,由关键字得到的哈希地址为j(0<=j<=n-1)的位置已存有记录,处理冲突就是为该关键字的记录找到另一个""的哈希地址。在处理中可能得到一个地址序列Hi i=12...k 0<=Hi<=n-1),即在处理冲突时若得到的另一个哈希地址H1仍发生冲突,再求下一地址H2,若仍冲突,再求H3...。怎样得到Hi呢?<o:p></o:p>

开放定址法:Hi=(H(k)+di) mod m  H(k)为哈希函数;m为哈希表长;di为增量序列)<o:p></o:p>

       di=123... m-1 时叫线性探测再散列。<o:p></o:p>

       di=12-1222-2232-32...,k2,-k2时叫二次探测再散列。<o:p></o:p>

       di=randomm)时叫伪随机探测序列。<o:p></o:p>

例如:长度为11的哈希表关键字分别为176029,哈希函数为H(k)=k mod 11,第四个记录的关键字为38,分别按上述方法添入哈希表的地址为843(随机数=9)。<o:p></o:p>

再哈希法Hi=RHi(key) i=1,2,...,k,其中RHi均为不同的哈希函数。<o:p></o:p>

链地址法:这种方法很象基数排序,相同的地址的关键字值均链入对应的链表中。<o:p></o:p>

建立公益区法:另设一个溢出表,不管得到的哈希地址如何,一旦发生冲突,都填入溢出表。<o:p></o:p>

1.3查找分析

一般情况下,处理冲突方法相同的哈希表,其平均查找长度依赖于哈希表的装填因子a<!---->

                                              a=表中填入记录数 / 哈希表的长度    

       a标志哈希表的装满程度。直观地看,a越小,发生冲突的可能性就越小;反之,a越大,表中已填入的记录越多,再填记录是,发生冲突的可能性就越大,则查找时,给定值需与之进行比较的关键字的个数也就越多。


 

随机探测的一组公式进行分析推导:

长度为m的哈希表中装有n个记录时查找不成功的平均查找长度(相当于要在这张表中填入第n+1个记录时所需要的比较次数的期望值) 

       假定:哈希函数均匀(即产生表中各个地址的概率相等),处理冲突后产生的地址也是随机的。

pi:表示前i个哈希地址均发生冲突的概率

qi:表示需进行i次比较才能找到一个“空位”的哈希地址(即前i-1次发生冲突,第i次不冲突),那么:

<o:p> </o:p>



2           二叉树

我们将讨论的二叉树分成三类:判定树,二叉排序树(搜索树),一般二叉树。

判定树:在折半查找时,我们依次比较的关键字的位置构成一个判定树,其节点的值就是记录在表中的位置。中序遍历判定树,就得到一个递增的有序序列。

二叉排序树:或者是一棵空树,或者是具有一下性质的二叉树:(1)若左子树不空,则左子树上所有节点的值均小于它的跟节点的值;(2)若右子树不空,则右子树上所有节点的值均大于他的跟节点的值;(3)它的左右子树分别也是二叉排序树

判定树与二叉排序树的联系是:中序都能得到有序序列,但是前者的节点值是位置,而后者的是关键字,并且,前者只是用于在折半查找是用于分析平均查找长度,并不是一个实实在在的树。

2.1二叉排序树的插入与删除

1、插入算法描述:

<o:p> </o:p>

插入一个新的节点时,需要进行关键的比较,并且维护二叉排序树的性质。

  1. int InsertBS ( BiTree T, Binode * x)
  2. {
  3.       /*在一棵已知二叉排序树中插入一个新的节点x*/
  4.        if (T= = NULL) T = x;; /* 一直二叉排序树为空时 */
  5.            return 1;
  6.        else Binode* p = T,q;
  7.        while(p!=NULL&&p.key!=x.key) /*只有在待插的节点关键字和树中某节点关键字不相等时,才进行相应的插入操作*/
  8.        {
  9.               if (p.key>x.key)
  10.                      {q = p;
  11.                      p = q->right;}
  12.               else
  13.                      {q = p;
  14.                      p = q->left;}
  15.        }
  16.        if(q.key= =x.key)  ERROR(“The same key has existed!”);
  17.        else
  18.               if  (q.key>x.key)  {q->left = x; return 1;}  /*插入节点为左节点*/
  19.               else         {q->right = x; return 1;}  /*插入节点为右节点*/
  20. }
  21.  

2、删除

删除操作分三种情况讨论:

(1)       删除节点p 叶子节点,则直接删除节点即可,不会破坏整棵树的结构。

(2)       删除节点p 不是叶子节点,只有左子树或者右子树,此时也可以直接修改其双亲节点的左子树或者右子树即可,作此修改也不会破坏二叉排序树的特性。

(3)       删除节点p的左右子树均不为空,此时,情况比较复杂一些,为保证二叉树的中序序列,可以有两种做法:其一是 p的左子树为p的父亲节点的左子树,p的右子树为p的直接前驱的右子树。其二(我们讨论的)是令p的直接前驱(或者直接后继)替代p,然后从二叉树中删除它的直接前驱(或者直接后继)。

算法如下:

<o:p>

  1. Void Delete ( BiTree &p)
  2. {
  3.        If (!p->right) /*右子树为空则重接左子树*/
  4.               {q = p; p = p ->left; free (q);}
  5.        Else  if (!p->left) /*左子树为空则重接右子树*/
  6.               {q = p; p = p ->right; free (q);}
  7.        Else /* 左右子树均不为空*/
  8.               {q = p;
  9.               s = p->left;
  10.               while (s->right)   /* 左拐,然后向右走到尽头 */
  11.               {
  12.                      q = s;
  13.                      s = s->right;
  14.               }
  15.               p.key = s.key;   /* s 指向被删除节点的前驱*/
  16.               if(q!=p) q->right = s->left; /*重接q的右子树 */
  17.               else q->left = s->left;    /*重接q的左子树 */
  18. }
</o:p>

2.2 二叉树的中序直接前驱和直接后继

本来是讨论二叉排序树的删除操作的,结果经HZP一提醒,大家就进一步分析了如何找到给定节点的中序直接前驱和直接后继,不错不错,由深而广。

算法如下:

  1. BiTree* findSuccssor( BiTree &p)
  2. {
  3.        If (p->right!=NULL) /*如果右子树,则其右子树的根节点就是其后继*/
  4.               {return p->right;}
  5. /*如右子树为空,则向上往左拐,到尽头节点为某个节点的左孩子,该节点就是后继*/
  6.        q = p->parent;
  7.                      While(q!=NULL&&p = = q->right)
  8.                      {
  9.                             p = q;
  10.                             q = q->parent;
  11.                      }
  12.        Return q;
  13. }
  14.  
  15. BiTree* findPredecessor( BiTree &p)
  16. {
  17.  If(p->left != NULL) /*如果左子树不为空,向左拐,向右走到尽头那个节点就是其前驱*/
  18.        {
  19.               q = p->left;
  20.               While(q->right)
  21.               { q = q->right; }
  22.         Return q;
  23.        }
  24.  Else /*如左子树为空,则向上往右拐,到尽头节点为某个节点的右孩子,该节点就是前驱*/
  25.        {
  26.               q = p->parent;
  27.               while(q!=NULL && p= = q->left)
  28.               {
  29.                             p = q;
  30.                             q = q->parent;
  31.               }
  32.        Return q;
  33.        }
  34. }

2.3 一般二叉树的非递归遍历

二叉树的非递归遍历有两种思路:一种是用直接实现递归遍历,即递归遍历中的那些栈都显示实现;二是用比较自然的遍历思想,也用栈实现。

1 先序遍历<o:p></o:p>


       preOrder1每次都将遇到的节点压入栈,当左子树遍历完毕后才从栈中弹出最后一个访问的节点,访问其右子树。在同一层中,不可能同时有两个节点压入栈,因此栈的大小空间为O(h)h为二叉树高度。时间方面,每个节点都被压入栈一次,弹出栈一次,访问一次,复杂度为O(n) LF原创)

<o:p>

  1. Void preOrder1(BiTree* root)
  2. {
  3.        Stack S;
  4.        While(root!=NULL || !S.empty())
  5.        {    
  6.               If(root!=NULL)
  7.                      {
  8.                             Visit(root); /*访问根节点*/
  9.                             Push(root); /*将根节点入栈,目的是为了找右子树*/
  10.                             root=root->left; /*依次访问左子树*/
  11.                      }
  12.               Else
  13.                      {
  14.                             root = S.pop(); /*回到父节点,*/
  15.                             root = root->right; /*要开始访问右子树*/
  16.                      }
  17.        }
  18. }
</o:p>

  1. Void preOrder2(BiTree* root)
  2. {
  3.        If(root!=NULL)
  4.        {
  5.               Stack  S;
  6.               Push(root);
  7.               While(!S.empty())
  8.               {
  9.                      BiTree * node  = s.pop();
  10.                      Visit(node); /*先访问根节点,无需压栈*/
  11.                      S.push(node->right);
  12.                      S.push(node->left);
  13.               }
  14. .      }
  15. }

       preOrder2每次将节点压入栈,然后弹出,压右子树,再压入左子树,在遍历过程中,遍历序列的右节点依次被存入栈,左节点逐次被访问。同一时刻,栈中元素为m-1个右节点和1个最左节点,最高为h。所以空间也为O(h);每个节点同样被压栈一次,弹栈一次,访问一次,时间复杂度O(n) LF原创)

2         中序遍历<o:p></o:p>

  1. void InOrder1(TNode* root)
  2. {
  3.     Stack S;
  4.     if( root != NULL )
  5.     {
  6.         S.push(root);
  7.     }
  8.     while ( !S.empty() )
  9.     {
  10.         TNode* node = S.pop(); 
  11.         if ( node->bPushed )
  12.         {   // 如果标识位为true,则表示其左右子树都已经入栈,那么现在就需要访问该节点了
  13.             Visit(node);        
  14.         }
  15.         else
  16.         {   // 左右子树尚未入栈,则依次将 右节点,根节点,左节点 入栈
  17.             if ( node->right != NULL )
  18.             {
  19.                 node->right->bPushed = false// 左右子树均设置为false
  20.                 S.push(node->right);
  21.             }
  22.             node->bPushed = true;  // 根节点标志位为true
  23.             S.push(node);
  24.             if ( node->left != NULL )
  25.             {
  26.                 node->left->bPushed = false;
  27.                 S.push(node->left);
  28.             }
  29.         }
  30.     }
  31. }

对比先序遍历,这个算法需要额外的增加O(n)的标志位空间。另外,栈空间也扩大,因为每次压栈的时候都压入根节点与左右节点,因此栈空间为O(n)。时间复杂度方面,每个节点压栈两次,作为子节点压栈一次,作为根节点压栈一次,弹栈也是两次,时间复杂度较高。

<o:p>

  1. Void InOrder2(BiTree* root)
  2. {
  3.        Stack S;
  4.        While(root!=NULL || !S.empty())
  5.        {    
  6.               If(root!=NULL)
  7.                      {
  8.                             Push(root);
  9.                             root=root->left; /*左子树入栈*/
  10.                      }
  11.               Else
  12.                      {
  13.                             root = S.pop();
  14.                             Visit(root); /*访问根节点*/
  15.                             root = root->right; /*通过下一次想循环实现右子树遍历*/
  16.                      }
  17.        }
  18. }
</o:p>

Inorde2类似于Preorder1,只是调换了一下节点的访问顺序。先序是先访问,再入栈,而中序是先入栈,弹栈后再访问。时空复杂度同先序一致。<o:p></o:p>

3        后序遍历<o:p></o:p>

  1. void PostOrder(TNode* root)
  2. {
  3.     Stack S;
  4.     if( root != NULL )
  5.     {
  6.         S.push(root);
  7.     }
  8.     while ( !S.empty() )
  9.     {
  10.         TNode* node = S.pop(); 
  11.         if ( node->bPushed )
  12.         {   // 如果标识位为true,则表示其左右子树都已经入栈,那么现在就需要访问该节点了
  13.             Visit(node);         
  14.         }
  15.         else
  16.         {   // 左右子树尚未入栈,则依次将 右节点,左节点,根节点 入栈
  17.             if ( node->right != NULL )
  18.             {
  19.                 node->right->bPushed = false// 左右子树均设置为false
  20.                 S.push(node->right);
  21.             }
  22.             if ( node->left != NULL )
  23.             {
  24.                 node->left->bPushed = false;
  25.                 S.push(node->left);
  26.             }
  27.             node->bPushed = true;            // 根节点标志位为true
  28.             S.push(node);
  29.         }
  30.     }
  31. }

后序遍历只采用了我们讨论的非递归的第一种思想,即直接模拟递归调用的思想来完成。与中序类似,就是顺序调换一下,时间空间复杂度也一致。

<o:p> </o:p>

以前的非递归遍历都没写过算法,多亏了LF&HZP两个,那个直接模拟的思想给我和Z两个共讲了三遍,总算明白了!真是两个大笨蛋!呵呵!(Z不要介意哈!)


分享到:
评论

相关推荐

    二叉树中两结点最近的共同祖先算法

    例如,我们可以使用哈希表来存储每个节点的祖先节点,以便快速查找两个节点的最近公共祖先。此外,我们还可以使用动态规划的方法来实现算法,以减少计算的次数。 数据结构分析 在设计算法时,我们需要选择合适的...

    数据结构学习笔记

    ### 数据结构核心知识点详解 #### 一、绪论 **数据结构的基本概念** ...无论是链表、哈希表、二叉树还是图,每种数据结构都有其独特的优势和应用场景。掌握这些基础知识对于开发高效算法和优化软件性能至关重要。

    算法大全-面试题-链表-栈-二叉树-数据结构

    - **删除重复元素**:通常使用哈希集合来辅助,记录已出现过的元素,遇到重复就删除。 这些链表操作是面试中常见的问题,对于程序员来说,理解和掌握这些基本操作是至关重要的,因为它们不仅涉及到基础的编程技巧...

    (完整word版)四川大学计算机学院数据结构与算法分析期末试题(2013级A).docx

    本试题涵盖了数据结构与算法分析的多个方面,包括树、图、查找、排序、哈希表等。以下是对每个问题的解读和知识点总结: 一、单项选择题 1. 在一棵高度为5的二叉树中,所含结点个数最多为32。 知识点:二叉树的...

    SHW2树应用类讨论题1

    在哈希表中,通过映射(Map)记录短信出现的频率。最后,用优先队列来存储频率最高的短信,这样可以在O(logn)的时间复杂度内找到重复最多的20条短信,大大提高了效率。 综上所述,解决千万条短信查重问题,哈希法、...

    数据结构课程设计(用C++编写的航班信息查询)

    - **哈希表**:如果查询主要基于特定的航班号,可以利用哈希函数将航班号映射到索引,实现近乎常数时间的查找速度,但需要处理哈希冲突。 2. **文件存储**: 除了内存中的数据结构,还需要考虑航班信息的持久化...

    数据结构练习记录.zip

    哈希表在查找、插入和删除操作上通常具有O(1)的时间复杂度,但可能会遇到哈希冲突问题。 8. **堆**:堆是一种特殊的树形数据结构,满足堆属性(最大堆中父节点的值大于或等于子节点,最小堆反之)。堆常用于实现...

    数据结构与算法分析(Java版)

    《数据结构与算法分析(Java版)》不仅涵盖了数据结构与算法的基本概念,还深入讨论了高级主题,如红黑树、哈希表和图论等。书中通过大量的例子和直观的图形解释,使得复杂的概念变得容易理解。此外,本书还提供了一...

    数据结构课件(清华大学版本的)

    然而,哈希冲突是哈希表面临的主要问题,解决冲突的方法有开放寻址法、链地址法等。 最后,哈弗曼树(又称最优二叉树)是一种带权路径长度最短的二叉树,常用于数据压缩和文件编码。构建哈弗曼树的过程就是哈弗曼...

    大数据结构程序设计题目共29题.docx

    这些题目涵盖了大数据结构程序设计的多个核心概念,包括数据结构(如链表、二叉树、哈希表、数组)、算法(如排序、查找、图的遍历)、以及实际应用(如操作系统、数据库管理)。以下是对这些题目的详细解读: 1. *...

    学生成绩管理系统_数据结构_

    在这个系统中,数据结构可能是数组、链表或者更复杂的数据结构如二叉树或哈希表,用于存储学生的个人信息和成绩信息。 1. **数组**:数组是最基础的数据结构,它允许我们通过索引来访问和修改元素。如果学生成绩...

    数据结构课程设计实验报告(详细)

    6. **哈希表**:哈希表通过散列函数将数据映射到数组,实现快速查找、插入和删除操作。解决哈希冲突的方法有开放寻址法和链地址法。 7. **堆**:堆是一种特殊的树形数据结构,满足堆属性(最大堆或最小堆)。堆常...

    查找(2)动态查找.pdf

    查找技术是数据结构和算法中的重要...对于动态查找表的实现,二叉排序树、平衡二叉树、B-树、B+树以及哈希表是几种常见的数据结构,它们各自具有不同的特点和适用场景,合理选择和使用这些技术能够有效提高查找效率。

    算法导论课后习题与思考题答案合集

    5. **数据结构**:了解不同数据结构(如数组、链表、栈、队列、哈希表、二叉树、红黑树等)的特点及其应用场景。 6. **动态规划**:掌握动态规划的基本思想,能够识别并解决常见的动态规划问题。 7. **贪心算法**:...

    数据结构考试各章自测题(有答案)

    哈希表提供快速的查找和插入功能,基于键值对的存储和查找时间复杂度可达到O(1);位图则用于高效处理大量布尔值,如在搜索引擎中进行关键词索引。 在自测题中,学生可能需要掌握这些数据结构的定义、性质、操作及其...

    数据库 最好的最快来下啊

    在本文中,我们将讨论数据库的一些核心概念以及与数据结构相关的技术,包括赫夫曼编码、表达式求值和哈希表。 首先,赫夫曼编码是一种用于数据压缩的高效算法,特别是在文本编码中非常有用。它基于构建赫夫曼树的...

    代码随想录-最强八股文(第五版)

    算法与数据结构是所有程序员的基础,八股文中可能包含数组、链表、栈、队列、哈希表、二叉树、图、排序算法(如快速排序、归并排序)和查找算法(如二分查找)等经典知识的分析与应用实例。 最后,软件工程和最佳...

    《数据结构与算法分析C++描述第三版》答案

    哈希表利用哈希函数将键映射到特定的位置上,从而快速访问记录。本章可能会介绍不同的哈希函数设计方法以及处理冲突的策略,例如开放地址法和链地址法。哈希表因其查找时间复杂度接近O(1)而在实际应用中极为常见。 ...

    C++实现的网上拍卖系统

    3. **出价逻辑**:拍卖的核心在于出价,我们可以维护一个以商品ID为键的哈希表,其值是一个双向链表,链表中的节点表示每一次的出价记录,按照出价时间排序。这样,每次新出价时,可以快速找到当前最高价,并更新。 ...

    数据结构实验报告模板

    5. **哈希表**:理解哈希函数,实现动态哈希表,解决冲突问题。 **四、实验结果与分析** 这部分详细记录每项实验的实现过程,代码示例,以及运行结果。同时,对实验中遇到的问题、解决方案及优化策略进行分析。比如...

Global site tag (gtag.js) - Google Analytics