读书笔记 --《大话数据结构》

读书笔记 --《大话数据结构》_第1张图片

大话数据结构

数据结构绪论

数据结构

逻辑结构

指数据对象中数据元素之间的相互关系逻辑结构分为以下四种:集合结构: 集合机构中的数据元素除了同属于一个集合外,它们之间没有其他关系。线性结构: 线性结构中的数据元素之间是一对一的关系。树形结构: 树形结构中的数据元素之间存在一种一对多的层次关系。图形结构: 图形结构的数据元素是多对多的关系。

物理结构

物理结构是指数据的逻辑结构在计算机中的存储形式。数据是数据元素的集合,那么根据物理结构的定义,实际上就是如何把数据元素存储到计算机的存储器中。存储器主要是针对内存而言的,像硬盘,软盘,光盘等外部存储器的数据组织通常用文件结构来描述。数据的存储结构应正确反映数据元素之间的逻辑关系,这才是最为关键的,如何存储数据元素之间的逻辑关系,是实现物理结构的重点和难点。数据元素的存储结构形式有两种:顺序存储(数组): 把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。链式存储结构: 把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。 这种存储结关系不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关数据元素的位置。

概念及术语

数据

描述客观事物的符号,指计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据不仅仅包括整形,实型等数据值类型,还包括字符及声音,图像,视频等非数值类型

数据对象

性质相同的数据元素的集合。是数据的子集。什么叫做性质相同呢,是指数据元素具有相同数量和类型的数据项,比如人都有姓名,生日,性别等相同的数据项。既然数据对象是数据的子集,在实际应用中,处理的数据元素通常具有相同性质,在不产生混淆的情况下,我们都将数据对象简称为数据。

数据元素

组成数据的,有一定意义的基本单位,在计算机中通常作为整体处理。也被称为记录。比如:在人类中,数据元素就是人。在禽类中,猪狗牛羊就是禽类的数据元素。

数据项

一个数据元素可以由若干个数据项组成。数据项是数据不可分割的最小单位。比如:人这样的数据元素,可以有眼,耳,鼻,嘴这些数据项,也可以有姓名,年龄等数据项。

数据结构

是相互之间存在一种或多种特定关系的数据元素的集合。

抽象数据类型

指一组性值相同的值的集合及定义在此集合上的一些操作的总称。

原子类型

不可以再分解的基本类型,包括整型,实型,字符型等

结构类型

由若干个类型组合而成,是可以再分解的。例如,整型数组是由若干整型数据组成的。

线性表

线性表:零个或多个数据元素的有限序列元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继。

顺序存储结构

(数组)指的是用一段地址连续的存储单元依次存储线性表的数据元素。随机存取结构。优点:无须为表示表中元素之间的逻辑关系而增加额外的存储空间。可以快速地存取表中任一位置的元素。缺点:插入和删除操作需要移动大量元素。当线性表长度变化较大时,难以确定存储空间的容量造成存储空间的“碎片”

插入与删除

插入算法的思路:如果插入位置不合理,抛出异常。如果线性表长度大于等于数组长度,则抛出异常或动态增加容量。从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置。将要插入的元素填入位置i处。表长加1。时间复杂度为O(n)删除算法的思路:如果删除位置不合理,抛出异常。取出删除元素。从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置。表长减1。时间复杂度为O(n)

读取

对于每个顺序存储结构的线性表位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常数,用时间复杂度来表示,它的存取时间性能为O(1).我们通常把具有这一特点的存储结构称为随机存取结构。

链式存储结构和顺序存储结构的比较

链表和数组的比较链表和数组是两种截然不同的内存组织方式,正因如此,它们插入、删除、随机访问的时间复杂度正好相反。数组使用的是连续的内存空间,可以利用空间局部性原理,借助 CPU cache进行预读,所以访问效率更高。而链表不是连续存储,无法进行缓存,随机访问效率也较低。数组的缺点是大小固定,一经声明就要占用整块连续的内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间用于分配,就会导致“内存不足(out of memory)”。而如果声明的数组过小,当不够用时,又需要重新申请一块更大的内存,然后进行数据拷贝,非常费时。而链表则没有大小限制,支持动态扩容。当然,因为链表中每个结点都需要存储前驱 / 后继结点的指针,所以内存消耗会翻倍。而且,对链表频繁的插入、删除操作会导致频繁的内存申请和释放,容易造成内存碎片和触发垃圾回收(Garbage Collection, GC)数组和链表插入删除操作的时间复杂度对比:在只知道下标时数组:O(n)       需要移动其后所有项的位置链表:O(n)       需要遍历其前面所有项以得到下标对应的项在拥有要操作的项的引用时数组:O(n)       需要移动其后所有项的位置链表:O(1)       无需遍历其实直接就插入删除的执行函数来看的话,链表和数组在只知道下标的情况下,其时间复杂度都是O(n),性能上是不会有太大差别的。因为数组不需要遍历,能直接取得要操作的对象,但是需要移动其后的所有项,而链表无法直接获取要操作的对象,需要遍历获取,之后直接删除或者插入,而不需要移动其他项。之所以说链表的插入和删除比数组的性能好,并不是说在任何情况下链表的插入和删除效率都要比数组的高,而是链表插入删除的最差时间复杂度也就是O(n),而在已得到要操作的结点的引用时,它就能省去遍历的步骤直接插入删除,时间复杂度为O(1),并且数组会有比如扩容等操作造成很多额外的时间支出,以及内存碎片所导致的空间支出。倘若不考虑这些,在只有下标的情况下执行插入和删除,它们的性能其实是没有太大区别的,就时间复杂度上来看都是O(n),然而实际情况下是不可能不考虑这些的。所以我们可以简单的记住结论:插入删除:链表性能好查询修改:数组性能好

链式存储结构

链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。因此,为了表示每个数据元素a1与其直接后继数据元素an+1之间的逻辑关系,对数据元素a1来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,……,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。链表中第一个节点的存储位置叫做头指针。有时我们会在单链表的第一个结点前设置一个结点,称为头结点。对于插入或删除数据越频繁的操作,单链表的效率优势就越明显。

头指针和头结点

头指针:头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。头指针具有标识作用,所以常用头指针冠以链表的名字。无论链表是否为空,头指针均不为空。头指针是链表的必要元素。头结点:头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)。有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了。头结点不一定是链表的必要元素。

读取

获取链表第 i 个数据的算法思路:声明一个结点p指向链表第一个结点,初始化 j 从1开始。当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1。若到链表末尾p为空,则说明第 i 个元素不存在。否则查找成功,返回结点p的数据。时间复杂度为O(n)。

插入

单链表第 i 个数据插入结点的算法思路:声明一结点 p 指向链表第一个结点,初始化 j 从 1 开始。当 j<i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j累加 1 。若到链表末尾p为空,则说明第 i 个元素不存在。否则查找成功,在系统中生成一个空结点s。将数据元素e赋值给s。单链表的插入标准顺序,先让结点s的指针指向后一个元素,然后让前一个元素指向结点s。返回成功。在只能得到要插入的位置i时,时间复杂度为O(n),而在能得到插入位置结点时,时间复杂度为O(1),因为不需要执行查询操作。

删除

单链表第 i 个数据删除结点的算法思路:声明一结点p指向链表第一个结点,初始化 j 从1开始。当 j<i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j累加 1 。若到链表末尾p为空,则说明第 i 个元素不存在。否则查找成功,将欲删除结点赋值给q单链表的删除标准顺序将要删除的结点的上一结点的指针指向要删除结点的指针(绕过删除结点)。释放q结点。返回成功。在只能得到要删除的位置i时,时间复杂度为O(n),而在能得到删除位置上一结点时,时间复杂度为O(1),因为不需要执行查询操作。

循环链表

将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。循环链表解决了一个很麻烦的问题,那就是可以从任意一个结点出发,访问到链表的全部结点。

双向链表

双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。

串是由零个或多个字符组成的有限序列,有名叫字符串。特殊概念:空格串:只包含空格的串,它与空串是有区别的,空格串是有内容有长度的,而且可以不止一个空格。子串与主串:串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串。子串在主串中的位置就是字串的第一个字符在主串中的序号。

串的比较

字符串的比较,它们在计算机中的大小其实取决于它们挨个字母的前后顺序。比如“silly” 和 ”stupid“ 这两个比较,第一个字母一样,不存在大小差异,而第二个字母,由于 “i” 字母比 “t”字母要靠前,所以 “i” < “t” ,于是我们说 “silly” < ”stupid“ 。事实上,串的比较是通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应字符集中的序号。既,只有每个位置上的字符都相等,呢么它们两个字符串才能被判定为相当。而只要某一个位置上的字符不相同,或者其字符串长度不相同,呢么它们就不相同。

匹配算法

子串的定位操作通常称做串的模式匹配。

朴素的模式匹配算法

子串的定位操作通常称做串的模式匹配。先找到子串的第一个字符在父串中的位置,然后按子串的长度对比其父串中确定位置后面的对应字符,若其后长度不足子串长度,则直接判读为false。

KMP模式匹配算法

其思想主要是利用子串中各个字符的相同,来经可能减少字串在父串中的匹配次数。过于复杂,暂时不深究算法实现。

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

各种图定义

无向边(无边用小括号表示):若顶点Vi 到 Vj 之间的边没有方向,则称这条边为无向边,用无序偶对(Vi , Vj)来表示 。有向边<有向边用尖括号表示>:若从顶点A 到 B 的边有方向,则称这条边为有向边,也成为弧。用有序偶<B , A>来表示,连接A到B的有向边就是弧,A是弧尾,D是弧头,<A , D>表示弧,注意不能写成<D,A>在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。有很少条边或弧的图称为稀疏图,反之称为稠密图。带权的图通常称为网。

排序

内排序

内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。对于内排序来说,排序算法的性能主要是受3个方面影响:时间性能 2. 辅助空间 3.算法的辅助性

外排序

外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。

常见排序算法

冒泡排序

 /**    * 冒泡排序    * 排序方法:倒数第一位和倒数第二位进行比较,小的放在前面,    * 然后倒数第二位和倒数第三位比较,小的放前面,    * 这样一轮排序完成后,数组第一位便是该数组中最小的值了,    * 然后进行第二轮排序,这次排序是排除了第一位,也就是已经确定是最小的值以外的其他值进行比较,排出第二位的值,    * 这样,排序进行数组长度-2次后,数组便排序完成了    */   private static void bubbleSort(int[] arry) {     int tem = 0;     for (int i = 0; i < arry.length - 1; i++) {       for (int j = arry.length - 1; j > i; j--) {         if (arry[j] < arry[j - 1]) {           tem = arry[j];           arry[j] = arry[j - 1];           arry[j - 1] = tem;         }       }     }     } /* * 冒泡排序优化 * 在原本的冒泡排序方法上,增加一个flag标记,这样就可以避免一部分没有必要的循环比较, * 既在某次比较后发现所有位置已经有序了,那么就可以跳过剩下的循环了 */ private static void bubbleSort(int[] arry) { int tem = 0; boolean flag = true; for (int i = 0; i < arry.length - 1 && flag; i++) { flag = false; for (int j = arry.length - 1; j > i; j--) { if (arry[j] < arry[j - 1]) { tem = arry[j]; arry[j] = arry[j - 1]; arry[j - 1] = tem; flag = true; } } } }

简单选择排序

/**    * 选择排序    * 排序方法:第一位和第二位进行比较,记下小的值的数组下标,    * 用其和第三位进行比较,同样记下小的值的数组下标,    * 这样一轮后,便得到最小数值的数组下标,将其与第一位交换,    * 然后将第一位排除在外,第二位开始重复以上排序。    * 原理上基本与冒泡排序相同,不同的是直接选择排序只在确定了其数值在数组中的位置后进行一次交换,    * 而冒泡排序进行多次交换,最后才确定其在数组中你的位置    */       private static void selectionSort(int[] arry) {     int k = 0;     int tem = 0;     for (int i = 0; i < arry.length - 1; i++) {       k = i;       for (int j = i; j < arry.length; j++) {         if (arry[j] < arry[k]) {           k = j;         }       }         tem = arry[i];       arry[i] = arry[k];       arry[k] = tem;     }     }

直接插入排序

 /**    * 插入排序    * 排序方法:先吧下标为1的值存起来,然后用下标为0的与其相比,    * 如果下标为1的小于下标为0的,则下标为0的后移一位,    * 存起来的值赋给数组下标为0的,    * 照此循环,当遇见前一位比后一位大时,就让前一位后移,一直到遇见比自己小的,确定其下标,吧存的值赋值给其后一位,     */       private static void insertSort(int[] arry) {         int tem = 0;     for (int i = 1; i < arry.length; i++) {       tem = arry[i];       int j = i;       while (j > 0 && arry[j - 1] >= tem) {         arry[j] = arry[j - 1];         j--;       }       arry[j] = tem;     }   }

希尔排序

/**   * 希尔排序   * 基于插入排序,是插入排序的优化版,解决了插入排序有些情况下会进行多次移动的缺点   */   private static void shellSort(int[] arry) {  //初始化一个间隔    int h = 1;   //计算最大间隔     while (h < arry.length / 3) {       h = h * 3 + 1;     }     while (h > 0) {   //进行插入排序       int tem = 0;       for (int i = h; i < arry.length; i++) {         tem = arry[i];         int j = i;         while (j > h - 1 && arry[j - h] >= tem) {           arry[j] = arry[j - h];           j -= h;         }         arry[j] = tem;       }   //减少间隔       h = (h - 1) / 3;     }   }

堆排序

堆是具有下列性质的完全二叉树:每个结点的值都大于火等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。堆排序的时间复杂度为O( nlogn )。在性能上远远好过于冒泡,简单选择,直接插入的O(n2 )的时间复杂度。但由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。

归并排序

归并排序就是利用归并思想实现的排序方法。它的原理是假设初始序列含有 n 个记录,则可以看成是 n 个有序的子序列,每个子序列的长度为1,然后两两归并,得到【n/2】(【x】表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。

快速排序

/**    * 快速排序    * 通过将一个数组划分为两个子数组,然后通过递归调用自身为每一个子数组进行快速排序来实现    * 划分:确定一个关键数值,比数值小的放左边,比数值大的放右边    * 关键值:数组最左端的数作为关键值    */       private static void quickSort(int[] arry, int start, int end) {     //如果start > end,则说明数组全部值都进行过排序,跳出递归     if (start > end) {       return;     }     int i = start;     int j = end;     //确定关键值      int key = arry[start];     //完成一趟排序      while (i < j) {       //从左往右找到第一个大于或等于关键值的数        while (i < j && arry[i] <= key) {         i++;       }       //从右往左找到第一个小于关键值的数        while (j > i && arry[j] > key) {         j--;       }       //交换        if (i < j) {         int tem = arry[i];         arry[i] = arry[j];         arry[j] = tem;       }     }     //调整关键值的位置      int tem = arry[i];     arry[i] = arry[start];     arry[start] = tem;     //递归调用,对关键值左边的数快排      quickSort(arry, start, i - 1);     //递归调用,对关键值右边的数快排      quickSort(arry, i + 1, end);   }

排序性能比较

时间复杂度:冒泡排序: O( n2 )简单选择排序: O( n2 )直接插入排序: O( n2 )希尔排序: O( n3/2 ) 是一种不稳定的排序算法堆排序: O( nlogn ) 是一种不稳定的排序算法性能:优》劣堆排序》希尔排序》直接插入排序》简单选择排序》冒泡排序

算法基本概念

算法的特性

输入输出

算法具有零个或多个输入,算法至少有一个或多个输出。

有穷性

有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接收的时间内完成。

确定性

确定性:算法的每一步骤都具有确定的含义,不会出现二义性。

可行性

可行性:算法的每一步都必须是可行的,也就是说。每一步都能够通过执行有限次数完成。

函数的渐进增长

给定两个函数f(n)和 g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总比 g(n)大,那么,我们说f(n)的增长渐近快于 g(n)。判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。某个算法,随着n的增大,它会越来越优于另一算法,或者越来越差于另一算法。

算法时间复杂度

在进行算法分析时,语句的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随 n 的变法情况并确定 T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)= O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和 f(n)的增长率相同,称为算法的渐近时间复杂度,简称时间复杂度。其中f(n)是问题规模n的某个函数。用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。

推到大 O 阶方法

推导大O阶:用常熟1取代运行时间中的所有加法常数在修改后的运行次数函数中,只保留最高项如果最高阶项存在且不是1,则去除与这个项项相乘的常数。得到的结果就是大O阶。

推导例子

常数阶:(执行次数恒定,不会随着n的变化而发生变化)int sum = 0, n = 100 ; /*执行一次*/ sum = (1+n) * n/2 ; /*执行一次*/ System.out.println(sum); /*执行一次*/ 这个算法的运行次数函数是f(n)= 3.根据推导大O阶的方法,第一步把常数项3改为1,保留最高阶时发现,没有最高阶项,所以这个算法的时间复杂度为O(1).线性阶:for(int i=0 ; i<n ; i++){ /*时间复杂度为O(1)的程序步骤*/ } 这个算法的时间复杂度为O(n),因为循环体中的代码需要执行n次。对数阶:int count = 1 ; while( count < n ){ count = count * 2 ; } 由于每次count乘2之后,就距离n更近了一分,也就是说,有多少个2相乘后大于n,则会退出循环。由2x = n 得到x = log2n。所以这个时间复杂度为 O(logn) 。对于对数阶来说log的底数是不重要的,可以省略。平方阶:int i , j ; for(i = 0 ; i < n ; i++){ for(j = 0 ; j < n ; j++){ /*时间复杂度为O(1)的程序*/ } } 对于外层的循环,不过是内部这个时间复杂度为O(n)语句,再循环n次。所以这段代码的时间复杂度为O(n2)。复杂一点的例子:int i , j ; for( i = 0 ; i < n ; i++){ for( j = i ; j < n ; j++){ /*时间复杂度为O(1)的程序*/ } } 当i = 0 时,内循环执行了n 次,当i = 1 时,执行了n-1 次,…… 当i = n - 1 时,执行了1次。所以总的执行次数为:n+(n-1)+(n-2)+……+1= n(n+1)/2 = n2/2+n/2 (首位相加,既有n个n+1,因为用了两个该数,所以/2) 用推导大O阶的方法,只保留最高项,因此保留n2/2;去掉这个项相乘的常数,也就是去除1/2,最终这个代码的时间复杂度为O(n2).常见的时间复杂度:函数: 时间复杂度: 类型:12 O(I) 常数阶2n+3 O(n) 线性阶3n2+2n+1 O(n2) 平方阶5log2n+20 O(logn) 对数阶2n+3nlog2n+19 O(nlogn) nlogn阶6n3+2n2+3n+4 O(n3) 立方阶2n O(2n) 指数阶nlogn阶立方阶指数阶

算法空间复杂度

算法的空间复杂度通过计算算法所需要的存储空间实现,算法空间复杂度的计算公式记作:S(n) = O(f(n)),其中,n为问题规模,f(n)为语句关于n所占存储空间的函数。

栈与队列

栈的定义

先进后出,后进先出栈是限定仅在表位进行插入和删除操作的线性表允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表。栈的插入操作,叫作进栈,也称压栈,入栈。栈的删除操作,叫作出栈,也有的叫作弹栈。

栈的顺序存储结构

用数组实现,下标为0的位置作为栈底,因为其变化最小。定义一个top变量来表示栈顶位置。每次入栈出栈对top变量进行加减,使其始终指向栈顶。

两栈共享空间

顺序存储都有一个问题,为了分配连续的存储位置,必须事先确定大小,顺序栈也是如此,为了减少后期扩展的消耗,一般会设计得比预计大小要大上一些,然而在相同数据的栈时,可以试用共享空间的形式来增加空间的使用率,减少无用的空间损耗。既:把数组的头和尾,也就是数组下标为0和为n-1的位置,分别当作栈A和栈B的栈底,两个栈分别往中间添加数据做入栈操作,这样一来,两个栈就用了一个数组的内存,但是这必须是在两个栈存在彼增我减,彼减我增的关系时才能使用的方式,不然只是把原本两个数组变成一个大小为原本两个数组的一个大数组而已。打个比方:例如需要建立两个栈,一个栈表示已卖出产品,一个栈表示未卖出产品,而产品一共有1000个,呢么正常来说,我们为了避免后续出现数组扩充操作,我们只能定义两个大小为1000的栈,即使我们知道其中肯定有1000的内存会被浪费,可是我们还是必须这样做,因为确实存在全部货物都被卖出,或者全部货物都没卖出的情况,而这种情况下就会导致其中一个栈需要1000的内存,而另一个栈为0。而如果使用了两栈共享空间,既两个栈建立在一个数组上,我们就可以只定义一个大小为1000的数组了,因为它们两个栈存在彼增我减,彼减我增的关系,所以无论如何也不会使得总大小超过1000,极端情况下,货物被全部卖出或货物全部没卖出,也只是其中一个栈占用该数组全部空间,而另一个栈不占用而已。若这时候被卖出一个货物或者被退回一个货物,因为原本占满空间的栈需要出栈,所以另一个栈的入账也就有了空间。

栈的链式存储结构

队列

队列的定义

队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。允许插入的一端叫做队尾,允许删除的一端叫做队头。

循环队列

顺序队列通过数组实现:一开始时队头队尾都指向0,随着数据的插入,队尾往后添加,这样一来就会出现一个问题,容易出现内存浪费的情况。例如我们一开始定义数组的大小为5。我们添加了五个数据,然后再删除了四个数据时,这时候,数据里其实只有一个数据,有四个位置是空的,但是你已经无法继续添加数据了,因为这时候最后存在的数据已经位于数组尾部了,无法继续往后添加数据了,哪怕前面的数据已经被删除,有了空位。也就是所谓的 假溢出 。为了解决这个问题,循环队列也就应运而生,解决方法很简单,如果数组到了尾部无法再添加,而数组头部有空位,呢么就插入到头部就行了,至于如何判断头尾位置,长度,这些就要看循环队列的实现了。循环队列解决了一般顺序队列的假溢出问题,但是还是无法解决出现真正的数组溢出问题,这是顺序存储结构的通病了。

循环队列的长度

判断长度的公式: (rear - front + QueueSize)%QueueSize推导过程:当队列尾部在队列头的后面时,即rear > front,其长度为:rear - front当队列尾部在队列头的前面时,即rear < front,其长度为:分为两段:(QueueSize - front) + (0+rear)即: rear - front + QueueSize由此推到通用的长度计算公式,当 rear > front 时, rear - front < QueueSize故:(rear - front + QueueSize)%QueueSize = rear - front当 rear < front 时, rear - front > QueueSize故:(rear - front + QueueSize)%QueueSize = rear - front + QueueSize

循环队列判断是空还是满

循环列表与一般列表在判断为空还是满的情况有所区别。一般列表,当队头与队尾指向同一位置时,队列就是空的。而对于循环列表来说,队头与队尾指向同一位置时,队列可能为空,也可能是满了。对于判断循环队列是空还是满,可以通过两种设计方法来解决:设置一个标志变量,当头尾指向同一位置时,若该标准变量为true,则是空,否则就是满。这样一来,我们就需要在队列发送循环,即从尾部跳到头部时,来改变这个标准变量。当队列为空时,头尾指向同一位置,当队列满时,我们修改其条件,保留一个元素空间,即队列满时,数组中还有一个空闲单元,这样一来,头尾指向同一位置就只可能是队列为空了。

队列的链式存储

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。

树(Tree)是n(n》0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:有且仅有一个特定的称为根(Root)的结点当n》1时,其余结点可以分为m(m》0)个互不相交的有限集T1,T2,…… ,Tm,其中每一个集合本身又是一颗树,并且称为根的子树。关于互不相交这一点,意思是,一个子树上的结点不能连接到另一个子树上的结点,既从一结点出发,不能出现有另一路径循环会跑到自己身上的情况。

结点的分类

树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数称为结点的度(Degree)。度为0的结点称为叶结点(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点叶称为内部结点。树的度是树内各结点的度的最大值。既:一个树上存在某一结点拥有最多子树,呢么这个数量就是该树的度。

根结点:无双亲,唯一

叶结点:无孩子,可以多个

中间结点:一个双亲,一个或多个孩子

结点间的关系

结点的子树的根称为该结点的孩子(child),相应地,该结点称为孩子的双亲(parent)。为什么补上父或者母?因为对于结点来说其父母同体,唯一的一个,所以称为双亲。同一个双亲的孩子之间互称兄弟(sibling)。结点的祖先是从根到该结点所经分支上的所有结点,反之,以某结点为根的子树中的任一结点都称为该结点的子孙。

结点的层次

结点的层次从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第n层,则其子树的根就在第n+1层。其双亲在同一层的结点互为堂兄弟。树中结点的最大层次称为树的深度或高度。

有序树和无序树

如果将树中结点的各子树看成从左到右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。

森林

森林(forest)是m(M》0)棵互不相交的树的集合,对于树种每个结点而言,其子树的集合既为森林。

二叉树

二叉树(Binary Tree)是n( n>0 )个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两颗互不相交的,分别称为根结点的左子树和右子树的二叉树组成。特点:每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。左子树和右子树是由顺序的,次序不能任意颠倒。即使树中某结点只有一颗子树,也要区分它是左子树还是右子树。

特殊二叉树

二叉排序树

二叉排序树,又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值。若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值。它的左,右子树也分别为二叉排序树。二叉树的查找,修改,插入,都是十分简单的操作,比较麻烦的就是删除。删除结点会遇见三种情况:叶子结点。仅有左或右子树的结点。左右子树都有的结点。如果是叶子结点,呢么直接删除即可,因为它没有任何子结点,对二叉树没有结构上的影响。如果仅有左或右子树的结点,呢么将它的左子树或者右子树接到要删除的结点的父结点就行了。最麻烦的就是左右子树都有结点的情况,这时在要被删除的结点的子树中,找到它的直接前继和直接后继,用他们取代被删除结点。

平衡二叉树(AVL树)

平衡二叉树,是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1。高度或者深度:树中结点的最大层次称为树的深度或高度。判断一个二叉树是否是平衡二叉树,首先判断该二叉树是否是二叉排序树,然后再判断每一个结点的左子树和右子树的高度差是否至多等于1。

斜树

所有结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫做右斜树。这两者统称为斜树。

满二叉树

所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。特点:叶子只能出现在最下一层。出现在其他层就不可能达到平衡。非叶子结点的度一定是2。在同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多。

完全二叉树

对一棵具有n个结点的二叉树按层序编号,如果编号为i(1《i《n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。既由层级往下数,可以不断层的数完的,就是完全二叉树。满二叉树一定是一棵完全二叉树,但是完全二叉树不一定是满二叉树。

线索二叉树

二叉树中可能存在着很多空指针的位置,我们可以在这些位置存储他们的前驱后继的指针,用来减少我们找到某一个结点的父节点的步骤,充分利用存储位置,这种在空指针域处存储前驱后继的指针的二叉树,便称为线索二叉树。

赫夫曼树

最基本的压缩编码方法:赫夫曼编码其最基本的思想,就是,利用不同字符串或者字母的权重的不同,为其分配对应的编码,让权重大的字符串活字母能分配到短的编码,权重小的分配长的编码,这样一来,整体的编码长度就能短上许多。因为短的编码对应的字母出现得多,而编码长的字母出现得少。

赫夫曼树的构建

根据给定的n个权值(W1,W2,…… Wn)构成n棵二叉树的集合F={ T1,T2,……,Tn },其中每棵二叉树Ti中只有一个带权为Wi根节点,其左右子树均为空。在F中选取两颗根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根节点的权值为其左右子树上根节点的权值之和。在F中删除这两棵树,同时将新得到的二叉树加入F中。重复2和3步骤,直到F只含一棵树为止。这棵树便是赫夫曼树。

二叉树的性质

二叉树的第i层上至多有2i-1个结点(i》1)。深度为k的二叉树至多有2k-1个结点(i》1)。对于任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。具有n个结点的完全二叉树的深度(log2n)+1 如果有一棵有n个结点的完全二叉树(其深度为(log2n)+1)的结点按层序编号(从第1层到第(log2n)+1层,每层从左到右),对任一结点i(1《i《n)有:如果i=1,则结点i 是二叉树的根,无双亲;如果i》1,则其双亲是结点[ i/2 ] 。如果2i》n,则结点i 无左孩子(结点 i为叶子结点);否则其左孩子是结点 2i。如果2i+1》n,则结点i 无右孩子;否则其右孩子是结点2i+1 。

存储结构

二叉树因为其具有顺序和规律,所以可以用顺序存储结构实现,但是当二叉树不是完全二叉树时,会浪费一部分的存储空间,所以一般使用链式存储结构实现。

二叉树遍历

前序遍历

前序遍历:根结点 ---> 左子树 ---> 右子树若二叉树为空,则空操作放回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。在二叉树模型中的遍历顺序是:ABDGCEFpublic void preOrderTraverse(TreeNode node) { if (node != null) { System.out.print(node.data+" "); preOrderTraverse(node.left); preOrderTraverse(node.right); } }

中序遍历

中序遍历:左子树---> 根结点 ---> 右子树若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。在二叉树模型中的遍历顺序是:GDBAECFpublic void inOrderTraverse(TreeNode node) { if (node!= null) { inOrderTraverse(node.left); System.out.print(node.data+" "); inOrderTraverse(node.right); }

后序遍历

后序遍历:左子树 ---> 右子树 ---> 根结点若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。在二叉树模型中的遍历顺序是:GDBEFCApublic void postOrderTraverse(TreeNode node) { if (node!= null) { postOrderTraverse(node.left); postOrderTraverse(node.right); System.out.print(node.data+" "); } }

层序遍历

若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序堆结点逐个访问。public void levelTraverse(TreeNode root) { if (root == null) {return;} LinkedList<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { TreeNode node = queue.poll(); System.out.print(node.val+" "); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } }

树与二叉树的转化

树转二叉树:加线。所有兄弟结点之间加一条连线。去线。对线中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。层次调整。以树的根结点为轴心,将整棵树瞬时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子 二叉树转换为树:加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点,右孩子的右孩子结点,右孩子的右孩子结点。。。哈。反正就是左孩子的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。去线。删除原二叉树中所有结点与其右孩子结点的连线。层次调整。使之结构层次分明。

森林与二叉树的转化

森林转换为二叉树把每颗树转化为二叉树。第一颗二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连起来。当所有的二叉树连接起来后就得到了由森林转化来的二叉树。二叉树转化为森林从根结点开始,若孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除……,直到所有右孩子连线都删除为止,得到分离的二叉树。再把每棵分离后的二叉树转换为树即可。

多路查找树(B树)

多路查找树,其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。由于它是查找树,所有元素之间存在某种特定的排序关系。减少内存与外存交换数据的次数,比如数据库,因为数据量巨大,无法全部读到内存中操作,所以需要不断的进行内存与外存的交互从而达到查询或者其他操作。

2-3树

每一个结点都具有两个孩子(我们称它为2结点)或三个孩子(我们称为3结点)。一个2结点包含一个元素和两个孩子(或没有孩子),且与二叉排序树类似,左子树包含的元素小于该元素,右子树包含的元素大于该元素。不过与二叉排序树不同的是,这个2结点要么没有孩子,要有就有两个,不能只有一个孩子。一个3结点包含一小一大两个元素和三个孩子(或没有孩子),一个3结点要么没有孩子,要么具有3个孩子。如果某个3结点有孩子的话,左子树包含小于较小元素的元素,右子树包含大于较大元素的元素,中间子树包含介于两元素之间的元素。并且2-3树中所有的叶子都在同一层次上。

2-3-4树

2-3树的扩展,既扩展了4结点,一个4结点包含小中大三个元素和四个孩子(或没有孩子),一个4结点要么没有孩子,要么具有4个孩子。

B树

B树是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。结点最大的孩子数目称为B树的阶,因此,2-3树是3阶B树,2-3-4树是4阶B树。一个m阶的B树具有如下属性:如果根结点不是叶结点,则其至少有两棵子树每一个非根的分支结点都有k-1 个元素和k个孩子,其中[m/2]<=k<=m。每一个叶子结点n都有k-1个元素,其中[m/2]<=k<=m 。所有叶子结点都位于同一层次。所有的非终端结点中包括如下信息的数据(n,A0,K1,A1,K2,A2,….,Kn,An)其中:Ki(i=1,2,…,n)为关键码,且Ki < K(i+1),Ai 为指向子树根结点的指针(i=0,1,…,n),且指针A(i-1) 所指子树中所有结点的关键码均小于Ki (i=1,2,…,n),An 所指子树中所有结点的关键码均大于Kn.n 为关键码的个数。所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。

B+树

一棵m阶的B+树和m阶的B树的差异在于:有n棵子树的结点中包含有n个关键字。所有的叶子结点包含全部关键字的信息,及指向含这些关键字记录的指针,叶子结点本身依关键字的大小自小而大顺序链接。所有分支结点可以看成是索引,结点中仅含有其子树中的最大(或最小)关键字。这样的数据结构最大的好处就在于,如果是要随机查找,我们就从根结点出发,与B树的查找方式相同,只不过即使在分支结点找到了待查找的关键字,它也只是用来索引的,不能提供实际记录的访问,还是需要到达包含此关键字的终端结点。如果我们是需要从最小关键字进行从小到大的顺序查找,我们就可以从最左侧的叶子结点出发,不经过分支结点,而是延着指向下一叶子的指针就可遍历所有的关键字。B+树的结构特别适合带有范围的查找。比如查找我们学校18-22岁的学生人数,我们可以通过从根结点出发找到第一个18岁的学生,然后再在叶子结点按顺序查找到符合范围的所有记录。

查找

顺序查找

顺序查找又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行比较查找。事件复杂度为 O( n )for(int i = 0 ; i <list.size() ;i++){ list.get(i); }

有序表查找

要求表内数据必须是有序的

折半查找

折半查找技术,又称为二分查找。它的前提是线性表中的记录必须是关键码有序,线性表必须采用顺序存储。折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间纪律的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。int[] list = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; int key = 11; int low, mid, high; low = 0; high = list.length - 1; while (low <= high) { mid = (low + high) / 2; System.out.println("mid = " + mid); if (list[mid] == key) { System.out.println("成功找到 list[mid] = " + list[mid]); break; } else if (list[mid] < key) { low = mid+1; } else if (list[mid] > key) { high =mid-1; } }

插值查找

插值查找是在折半查找的基础上进行优化产生的。其时间复杂度也是O( logn ),但是对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好得多,反之,数组中如果分布类似{0,1,2,2000,2001,……,999998}这种极端补均匀的数据,用插值查找未必是很合适的选择。插值查找是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于插值的计算公式( key-list[low] ) / ( list[high]-list[low] ) int[] list = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; int key = 11; int low, mid, high; low = 0; high = list.length - 1; while (low <= high) { //折半查找 // mid = (low + high) / 2; //插值查找 mid =low + (high - low) * (key - list[low])/(list[high] - list[low]); System.out.println("mid = " + mid); if (list[mid] == key) { System.out.println("成功找到 list[mid] = " + list[mid]); break; } else if (list[mid] < key) { low = mid+1; } else if (list[mid] > key) { high =mid-1; } }

斐波那契查找

斐波那契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、····,在数学上,斐波那契被递归方法如下定义:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2) (n>=2)。该数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618)。既与折半查找,插值查找不同的,就是每次缩小的区域是利用斐波那契的黄金分割来缩小,就性能而已,大部分情况下优于折半查找,当有些情况下会差于折半查找。斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数F[n],将原查找表扩展为长度为F[n](如果要补充元素,则补充重复最后一个元素,直到满足F[n]个元素),完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,找出要查找的元素在那一部分并递归,直到找到。

代码示例

public class FibonacciSearch {          /**       * @param args       */       public final static int MAXSIZE = 20;          public static void main(String[] args) {           // TODO Auto-generated method stub           int[] f = fibonacci();           for (int i : f) {               System.out.print(i + " ");           }           System.out.println();              int[] data = { 1, 5, 15, 22, 25, 31, 39, 42, 47, 49, 59, 68, 88 };              int search = 39;           int position = fibonacciSearch(data, search);           System.out.println("值" + search + "的元素位置为:" + position);       }          /**       * 斐波那契数列       *        * @return       */       public static int[] fibonacci() {           int[] f = new int[20];           int i = 0;           f[0] = 1;           f[1] = 1;           for (i = 2; i < MAXSIZE; i++) {               f[i] = f[i - 1] + f[i - 2];           }           return f;       }          public static int fibonacciSearch(int[] data, int key) {           int low = 0;           int high = data.length - 1;           int mid = 0;              // 斐波那契分割数值下标           int k = 0;              // 序列元素个数           int i = 0;              // 获取斐波那契数列           int[] f = fibonacci();              // 获取斐波那契分割数值下标           while (data.length > f[k] - 1) {               k++;           }              // 创建临时数组           int[] temp = new int[f[k] - 1];           for (int j = 0; j < data.lengt敏感词emp[j] = data[j];           }              // 序列补充至f[k]个元素           // 补充的元素值为最后一个元素的值           for (i = data.length; i < f[k] - 1; i++) {               temp[i] = temp[high];           }              for (int j : temp) {               System.out.print(j + " ");           }           System.out.println();              while (low <= high) {               // low:起始位置               // 前半部分有f[k-1]个元素,由于下标从0开始               // 则-1 获取 黄金分割位置元素的下标               mid = low + f[k - 1] - 1;                  if (temp[mid] > key) {                   // 查找前半部分,高位指针移动                   high = mid - 1;                   // (全部元素) = (前半部分)+(后半部分)                   // f[k] = f[k-1] + f[k-1]                   // 因为前半部分有f[k-1]个元素,所以 k = k-1                   k = k - 1;               } else if (temp[mid] < key) {                   // 查找后半部分,高位指针移动                   low = mid + 1;                   // (全部元素) = (前半部分)+(后半部分)                   // f[k] = f[k-1] + f[k-1]                   // 因为后半部分有f[k-1]个元素,所以 k = k-2                   k = k - 2;               } else {                   // 如果为真则找到相应的位置                   if (mid <= high) {                       return mid;                   } else {                       // 出现这种情况是查找到补充的元素                       // 而补充的元素与high位置的元素一样                       return high;                   }               }           }           return -1;       }   } 

索引查找

索引分类

线性索引

线性索引就是将索引项集合组织为线性结构,也称为索引表

稠密索引

稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项。对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列。优点:因为索引表是有序的,所以可以使用折半查找等各种查找方式,加快查询速度缺点:如果数据集非常大,意味着索引也得同样的数据集长度规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降了。

分块索引

分块有序,是把数据集的记录分成了若干块,并且这些块需要满足两个条件:块内无序,既每一块内的记录不要求有序。当然,你若果能够让块内有序对查找来说更理想,不过这就要付出大量时间和空间的代价,因此通常我们不要求块内有序。块间有序,例如,要求第二块所有记录的关键字均要大于第一块中所有记录的关键字,第三块的所有记录的关键字均要大于第二块的所有记录关键字……因为只有块间有序,才有可能在查找时带来效率。对于分块有序的数据集,每个块对应一个索引项,这种索引方法叫做分块索引。一种分块索引的结构(并不一定是这样设计):每个分块索引的索引项结构分为三个数据项:最大关键码:存储每一块中的最大关键字,这样的好处就是可以使得在它之后的下一块中的最小关键字也能比这一块最大的关键字要大。存储了块中的记录个数,以便于循环时使用。用于指向快首数据元素的指针,便于开始对这一块中记录进行遍历。分块索引表中查找,分两步进行:在分块索引表中查找要查关键字所在的快。由于分块索引表是快间有序的,因此很容易利用折半,插值等算法得到结果。根据块首指针找到相应的块,并在块中顺序查找关键码,因为块中是无序的,因此只能顺序查找。

倒排索引

索引项的通用的结构是:次关键码记录号表其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。这样的索引方法就是倒排索引。倒排索引源于实际应用中需要根据属性(或字段,次关键码)的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的为止,因而称为倒排索引。倒排索引的优点:查找记录非常块,基本等于生成索引表后,查找时都不用去读取记录,就可以得到结果。倒排索引的缺点:记录号不定长,若是对多篇文章所有单词建立倒排索引,那每个单词都将对应相当多的文章编号,维护比较困难,插入和删除都需要做相应的处理。

树形索引

多级索引

散列表(哈希表)

散列技术即是一种存储方法,也是一种查找方法。散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系 f ,使得每个关键字 key 对应一个存储位置 f(key)。这种对应关系 f 称为散列函数,又称哈希(Hash)函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。那么关键字对应的记录存储位置我们称为散列地址。散列表查找步骤1.在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。2.当查找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。散列表技术最适合的求解问题是查找与给定值相等的记录,而不适合关键字包含大量数据的情况,也不适合范围性的查找。

散列表的冲突问题

开放定址法:所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。再散列函数法:事先准备多个散列函数,在发生散列地址冲突时,换一个散列函数计算。链地址法:既在发生散列地址冲突时,将数据当成链表接在原本数据之后,这样一来,就绝不会出现找不到地址的情况,当然,这也就带来了查找时需要遍历单链表的性能损耗。公共溢出区法:既单独为建立一个区域,存储所有发生冲突的数据,在查找时,通过散列函数计算出散列地址后,先与基本表的相应位置进行对比,如果相等,则查找成功;如果不相等,则到溢出表去进行查找。在冲突数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。

二叉树模型

二叉树模型

A(根)

B

D

null

G

null

C

E

F


 

你可能感兴趣的:(读书笔记 --《大话数据结构》)