数据结构总结

前言

  下面是我看了《大话数据结构》一书之后,做的笔记,引用了不少书中的话,和加上了一些自己的理解,这本书里面虽然不是同java写的,但是里面的举例说明很不错,边看书边上网查边敲代码,看完这本书,总算是对数据结构有一个比较全面的理解,到后面有机会的话会去深入的看一些算法的书的。

链表与数组

数组

  即线性表的顺序存储结构,指的是用一段地址连续的存储单元一次存储线性表的数据元素。

  • 列表内容

优点:查取,修改速度快;无须为表示线性表中元素中元素之间的逻辑关系而增加额外的存储空间。

  • 缺点:要一开始就确定存储空间容量,当元素数量超过容量时候不能扩容;插入和删除操作需要移动大量元素。
动态数组

  数组的改良版:主要是可以根据数组里面的元素数量动态改变数组容量。

java源码里面的代表:ArrayList<T>

链表

  即线性表的链式存储结构,由一堆结点相连组成,每个结点由它存放数据元素的数据域和存放后继结点地址的指针域组成。

  • 优点:插入删除速度快;不需要预先分配存储空间,有几个数据分配几个结点空间。
  • 缺点:查取,修改速度慢;因为有指针域的关系,每一个结点需要分配的空间花销比数组大

java中的结构:

    //节点结构
    private class Node<T>{
        private T data;
        private Node<T> next ;
        public Node(T data){
            this.data = data;
        }
    }
静态链表

  因为某些语言没有指针,用数组描述的链表就是静态链表。

循环链表

  将单链表中的终端节点由空指针改为指向头结点,就使单链表形成一个环,这种头尾相接单链表称为循环链表。

双向循环链表

  在循环链表的每个结点中,在设置一个指向其前驱结点的指针,就是双向循环链表。

java源码里面的代表:LinkList<T>

栈与队列

栈(先进后出)

  栈是限定仅在表尾进行插入和删除操作的线性表。
  栈有两种实现方式,一种是数组实现,一种是链表实现。

java源码里面的代表:Stack<E>使用数组实现的。

栈的应用:
  1. 实现递归:很多编译系统里面的递归都是用系统栈实现的————每次执行递归函数都先把原来的函数入栈,最后达到边界条件再返回一步一步出栈。
  2. 四则运算表达式求值:后缀表达式的运算,中缀表达式的运算。

队列(先进先出)

  队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
  队列也有两种实现方式,一种是数组实现,一种是链表实现。

java源码里面并没有给出队列的具体实现,只给出了一个接口Queue<E>和一个抽象类AbstractQueue<E>,如果想使用队列可以用实现了Queue接口的LinkedList<E>用作队列,使用里面的offer()和poll()方法入栈出栈,很多扩展如阻塞队列ArrayBlockingQueue<E>都是继承和实现它们的。

队列应用:
  1. 我们在很多应用和框架都能看到队列的影子,如Handler里面的MessageQueue,一些网络请求框架里面的请求队列等。

字符串

  字符串是由零个或者多个字符组成的有限序列。
  字符串也有两种实现方式,一种是数组实现,一种是链表实现。

java源码里面表示字符串的类有String(final类不可改变),StringBuffer(可改变、线程安全),StringBuilder(可改变、线程不安全)

字符串匹配算法

  是从一个字符串(主串)里面找出和另一个字符串(子串)完全相同的算法。

朴素字符串匹配算法

  BruteForce,暴力的意思,就是用蛮力进行字符串匹配,对主串的每隔一字符作为子串的开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符串开头做子串长度T的小循环,直到匹配成功或全部遍历完成为止。

KMP模式匹配算法

  相对于朴素算法,KMP算法更加高效,它改进了朴素算法,把朴素算法里面一些不必要的匹配去掉,大大避免了重复遍历。具体实现这里不介绍。

KMP算法中,长为n的字符串中匹配长度为m的子串的复杂度为O(M+N)。

树的定义

  树是n(n>=0)个结点的有限集。n=0时为空树。
  在n>1时:
1. 有且只有一个根结点。
2. 除了根节点其余可以分为其他的有限集,本身又是一棵树,称为子树。
3. 子树一定是互不相交的。

树的性质:

  • 度:结点的子树个数;
  • 树的度:树中任意结点的度的最大值;
  • 兄弟:两结点的parent相同;
  • 层:根在第一层,以此类推;
  • 叶子结点:在最底层的结点,没有子树。
  • 高度:叶子结点的高度为1,根结点高度最高;
  • 深度:就是根结点的高度,也是树中结点的最大层次。
  • 有序树:树中各个结点是有次序的;
  • 森林:多个树组成,是m(m>=0)棵互不相交的树的集合;

树的存储结构

  1. 双亲表示法:每个结点存储,数据、parent在数组中的下标。
  2. 孩子表示法:全部结点组成一个数组,每个数组指向一个单链表,存放其孩子。
  3. 双亲孩子表示法:双亲表示法和孩子表示法的结合。
  4. 孩子兄弟表示法:每个结点有两个指针,一个指向最左子结点,一个指向右兄弟结点。

二叉树

  是树的特例,是子结点最多只有两个的树。

二叉树的特点
  • 每个结点最多有两棵子树。
  • 左子树和右子树是有顺序的,次序不能颠倒。
  • 基石树中某结点只有一颗子树,也要区分左右。
特殊的二叉树
  1. 斜树:所有结点都只有左子树的二叉树叫左斜树,所有结点都只有右子树的二叉树叫右斜树。
  2. 满二叉树:所有的分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上的二叉树。
  3. 完全二叉树:只有最下面的两层结点度小于2,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。度为1的结点只有左孩子,同样节点数的二叉树,完全二叉树的深度最小。
二叉树的性质
  1. 在二叉树中,第i层的结点总数不超过2^(i-1);
  2. 深度为h的二叉树最多有2^h-1个结点(h>=1),最少有h个结点;
  3. 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1(这个可以通过方程————连接线数(2*N2+N1) + 根结点数(1) = 总结点数(N0 + N1 + N2) 来求出);
  4. 具有n个结点的完全二叉树的深度为int(log2n)+1;
  5. 有N个结点的完全二叉树各结点如果用层序编号存储,则结点之间有如下关系:
    • 若I为结点编号,如果I=1则,I为根结点, 如果I>1,则其父结点的编号为I/2;
    • 如果2*I <= N,则其左儿子(即左子树的根结点)的编号为2*I;若2*I>N,则无左儿子;
    • 如果2*I+1 <= N,则其右儿子的结点编号为2*I+1;若2*I+1 > N,则无右儿子。
二叉树的存储结构

  一般都是用链表来实现二叉树的结构,只有实现完全二叉树的才会采用数组实现。

    //节点结构
    class Node<T extends Comparable<T>>{
        public T value;
        public Node<T> left = null;
        public Node<T> right = null;
        public Node(T value){
            this.value = value;
        }
    }
二叉树的遍历(笔面试经常考)

  指从根结点出发,按照某种次序依次访问二叉树中所有结点,是的每个结点都被访问一次且仅被访问一次。

  • 层序遍历:从树的根结点开始,从上而下逐层遍历,在同一层按从左到右的顺序对每个结点进行遍历。

    只能用非递归实现,要用一个队列装准备被遍历的结点。

  • 前序遍历(根-左-右):先访问根结点,然后前序遍历左子树,然后前序遍历右子树,都是按照根-左-右的顺序来进行遍历的。

    可由递归和非递归方法实现,非递归需要用栈。

  • 中序遍历(左-根-右):先中序遍历根结点的左子树,然后访问根结点,然后中序遍历右子树,都是按照左-根-右的顺序来进行遍历的。

    可由递归和非递归方法实现,非递归需要用栈。

  • 后序遍历(左-右-根):先中序遍历根结点的左子树,然后中序遍历右子树,最后访问根结点,都是按照左-右-根的顺序来进行遍历的。

    可由递归和非递归方法实现,非递归需要用栈。

      前中后序三种遍历,只知道两种结果的话,只有知道前中序,和后中序才能根据结果写出原来的二叉树。

线索二叉树

  二叉树经过某种方式遍历之后,结点就变得有顺序了,结点就有了前驱和后继,如果结点有指针指向它的前驱或者后继,这些指针就称为线索,加上线索的二叉链表称为线索链表,对应的二叉树就是线索二叉树了。
  因为n个结点的二叉链表中含有n+1个空指针域,刚好可以这些空指针利用起来,用来装线索。
  线索二叉树根据遍历方式的不同,又分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。

线索二叉树把二叉树根据遍历方式把结点又串成一个单链表,解决了遍历之后寻找结点前驱后继的问题。

树、森林、二叉树的转换

树转为二叉树

  步骤:
1. 在所有兄弟结点之间加一条连线。
2. 对树中的每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子的连线
3. 按照第一个孩子是父结点的左孩子,兄弟结点转换过来的就当作右孩子的规律调整层次。

转换后该二叉树肯定没有右子树,因为根结点没有兄弟结点,根据步骤3,根结点没有右孩子。

森林转为二叉树

  因为森林就是若干棵树组成的,所以可以把森林中的每一棵树都当作是兄弟来按照树转二叉树的步骤3来转换,步骤:
1. 先把森林里的每棵树都转换为二叉树。
2. 第一棵树不动,然后依次把后一棵二叉树作为前一棵树的右子结点连接起来,也就是上面说的步骤3的规则。

二叉树转为树

  就是树转二叉树的逆过程,步骤:
1. 把二叉树里面的所有结点的左子结点的n个右子结点都连上这个结点。
2. 删除原二叉树中所有结点与其右子结点的连线。

二叉树转为森林

  就是森林转二叉树的逆过程,步骤:
1. 看根结点,如果有右子树,就把与它的连线删除分离,分离出来的二叉树如果还有右子树,继续分离。
2. 然后把分离出来的二叉树转为树。

赫夫曼树

  从树中一个结点到另一个几点之间的分支构成两个结点之间的路径,路径上的分支数目称作路径长度树的路径长度就是根到每一个结点的路径长度之和。如果树的路径有权重,则带权路径WPL最小的二叉树称作赫夫曼树。

赫夫曼树没有度为0的结点。

  图是由顶点集(非空)和边集组成的数据结构,通常便是为G(V,E)。

定义

  • 无向边:顶点Vi到Vj之间的边没有方向,用无序偶对(Vi,Vj)表示。
  • 有向边:和无向边相反,顶点Vi到Vj之间的边有方向,也称为弧,用有序偶对

图的存储结构

  1. 邻接矩阵:用两个数组来表示一个图,一个一维数组表示图中的顶点信息,一个二维数组表示图中边或弧的信息。
  2. 邻接表:数组与链表结合的存储方法。顶点用一个一维数组存储,每个元素有一个指向邻接点的邻接指针。每个顶点的所有邻接点构成一个链表,头部与顶点元素的邻接指针相连。(有向图有逆邻接表,为了方便获取顶点的入度)。
  3. 十字链表:把邻接表和逆邻接表结合在一起。优缺点:虽然建立的时候结构复杂一点,但是容易求得顶点的出度和入度。这是有向图的优化存储结构
  4. 邻接多重表:对于无向图,邻接表关注的重点是顶点的操作,邻接多重表关注的是边的操作。这是*无向图的优化存储结构。*
  5. 边集数组:用两个一维数组来表示一个图,一个表示图中的顶点信息,另一个存储边的信息。和邻接矩阵差不多,但是关注的重点在于边。

图的遍历

  从图中某一顶点触发访遍途中其余顶点,而且使每一个顶点仅被访问一次,这个过程叫做图的遍历。

深度优先遍历(DFS)

  这是一个递归的过程,和树的前序遍历差不多,从图中某个顶点v出发,然后从v的未被访问的零界点触发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。不断对相邻的顶点做深度优先遍历图算法,不断递归,直到有个顶点的相邻顶点全被访问完,就返回到还没有被访问完的顶点,直到返回到第一个顶点v,递归结束。

广度优先遍历(BFS)

  和树的层序遍历差不多,也是用一个队列装要被遍历的元素,从第一个顶点v开始,把它入队,然后把队头元素出队,进行遍历,再把出队的顶点的未被访问过的邻接顶点入队,然后出队,重复这个过程,直到队列为空,即代表全部遍历完成。

两种遍历的特点

  两种遍历算法的时间复杂度是一样的,不同之处仅仅在于对顶点访问的顺序不同。
- DFS:更适合目标比较明确,以找到目标为主要目的的情况。
- BFS:更适合在不断过大遍历范围时找到相对最优解的情况。

最小生成树

  构造联通网的最小代价生成树称为最小生成树。

普里姆算法

  以某个顶点为起点,逐步寻找各顶点上最小权值的边去构建最小生成树。(图的存储使用邻接矩阵比较适合)

克鲁斯卡尔算法

  以为目标,找到权值最小的边去构建最小生成树。(图的存储使用边集数组比较适合)

最短路径

  最短路径指网之间得的两个顶点之间经过的边上权值之和最少的路径。

迪杰斯特拉算法

  按路径长度递增的次序产生最短路径,基于已经求出的最短路径的基础上,求得更远顶点的最短路径,最终得出结果。

弗洛伊德算法

  使用两个二维数组,把最短路径的变化都在数组上操作,最后得出结果在数组上显示。

两种算法特点
  • 迪杰斯特拉算法:时间复杂度是O(n^2),解决只从某个人源点到某个顶点的最小路径。
  • 弗洛伊德算法:时间复杂度是O(n^3),解决所有顶点到所有顶点的最短路径。

排序算法

稳定性

  排序的稳定性是指如果数据有多个关键字可以排序,当根据A关键字排序之后,再根据B关键字排序时,B关键字相同的数据里面还是根据A关键字排序的位置摆放,就是所谓的相对次序保持不变。稳定性在多个关键字排序时候很关键。

外排序

  外排序是因为要排序的数据太多,不能同时放在内存中排序,排序过程中需要内外存之间的数据交换。

内排序

  内排序是将所有要排序的数据都放在内存里面排序。影响内排序的性能主要有两个因素:
- 时间性能(时间复杂度):是衡量排序算法好坏的最重要标志,所以经常有牺牲空间来换取时间性能的算法。
- 辅助空间(空间复杂度):辅助空间是处理存放要排序的数据之外,执行算法所需要的存储空间。

冒泡排序

  是一种简单的交换排序,:两路比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。

简单选择排序

  进行n-1轮比较,每一轮都选出最小值把它放在第n个位置。这样进行到最后就能得到有序的数据集。

直接插入排序

  假设数组前面的i个数据是排好的,把i+1位置的数据插入到已排好序的数组里面,这样从i=1开始不断循环直到i+1=数组长度n。

希尔排序

  就是插入排序的升级版,因为插入排序如果在排序前数据集的顺序是基本有序(小的关键字基本在前面,大的基本在后面,不大不小的基本在中间)的话,这样插入排序的效率是非常高的,所以希尔排序就是根据增量把数据集分割成好几组,把他们都进行插入排序,然后再缩小增量,继续这样插入排序,当最后增量为1时,所进行的插入排序所需时间就不用那么长了。

堆排序

  因为一个数组可以看成一颗完全二叉树,所以可以根据这个性质来建堆。在堆(假如是大顶堆)里,里面的每个节点都大于或等于其左右子节点的值。
  然后一开始把数组里面的元素调整为大顶堆的状态,取出顶部,和堆最后的交换(也就是把最大的放到数组尾部),然后进行调整,这样不断循环直到堆缩小为一个元素,堆排序就完成了。

归并排序

  利用归并的思想实现的排序:把有n个数据的数据集分割成n份有序的数据集,然后两两合并,得到的有序的数据集之后继续合并,最后合并成一个有序的n个元素的数据集。
  > 有一些改进了的递归算法就是把分割的最小粒度控制在一个比较小的范围,然后将得到的这些子数据集其他的简单排序如插入排序等,这样在数据量比较大的时候可以大大减少递归深度,增加效率。

快速排序

  快排在数据集里面找出一个关键字key,通过一趟排序将数据集分割成两部分,将比关键字小的放在一部分,将比关键字大的放在另一部分,然后分别对这两部分的数据进行快排,这样递归调用最后得到有序的数据集。

排序算法总结


  

查找算法

  查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素。

查找表

  1. 静态查找表:只作查找操作的查找表。
  2. 动态查找表:在查找过程中同时插入表中不存在的元素,或者从表中删除已经删除存在的某个数据元素。

顺序查找

  又叫线性查找,是最基本的查找技术:从表中的第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若相等则查找成功,返回记录,若直到最后都没找到,则查找不成功。

可以通过设置“哨兵”来减少对查找位置是否越界的判断。
也可以将经常用到的记录放在前面,不常用的放后面,效率就可以有大幅提高。

有序表查找

  这个对已经排序好的查找表进行查找的算法。

折半查找(二分查找)

  在(假设是升序的)有序表中,取中间记录与查找值比较,若相等,则说明查找成功,返回结果,若查找值小于它,则在中间记录的左半区域继续进行查找,反之则在右半区域。不断重复上面的步骤直到查找成功或者找完最小的区间,查找失败。

插值查找

  根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,核心是公式:mid = low + (key - a[low]) / (a[high] - a[low]) * (high - low),原来的折半查找是使mid值一直等于二分之一,但是因为查找值不一定靠近表的中部,所以就出现了这个算法。

斐波那契查找

  利用黄金分割原理来实现的查找算法,也是相当于二分查找的改善。首先要有个计算斐波那契数列的方法F(),然后对比要查找的数组的最大下标n(就是数组长度-1),看看n是处在斐波那契数列的哪两项F(k)和F(k-1)的数字之间,然后把数组的数目补全到那两项的后一项数字,方便分割,加上去的数据都是和数组的最后一项一样(例如n=10,就是处于F(6)=8和F(7)=13之间,就要把数组长度补到13个)。
  准备工作完成后就可以开始查找,先把分割点放在F(k-1)那里(按上面的例子来说是F(7-1)=8),这样就把数组分成了两个斐波那契数列数量的数组(左边是F(k-1),右边是F(k-2),这样一直递归下去),后面的也就按照二分查找那样的方法就行了,主要是划分方法不同。
  这样按照黄金分割比的划分就是如果处于右边半区域查找的话查找效率比较大,左边半区域的话查找效率比较小,但是平均效率比二分查找好。

斐波那契数列,又称黄金分割数列,指的是这样一个数列:0、1、1、2、3、5、8、13、21、……在数学上,斐波纳契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用

总结

  上面的三种有序查找方法都是通过划分区域来减少查找区域,本质上的不同就是分割点的选择不同:
- 二分查找就是根据中间点分割,需要加法和除法运算来实现。
- 插值查找根据比例划分,需要复杂的加减乘除来实现。
- 斐波那契查找根据黄金分割点来分割,需要的知识普通的加减法运算来实现,但是还需要调用一个斐波那契数列的方法辅助。
  

线性索引查找

稠密索引

  是指在线性索引中,将数据集中的每一个记录对应一个索引项,索引项一定是按照关键码有序的排列。

如果数据集非常大,那就意味着索引也需要同样长度的规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降了。

分块索引

  对数据集进行分块,使其分块有序(块内可以无序,块间要有序),然后在对每一块建立一个索引项,从而减少了索引项的个数。

分块索引在兼顾了对细分块不需要有序的情况下,大大增加了整体查找的速度,所以普遍被用于数据库表查找等技术的应用当中。

倒排索引

  在记录数据的时候吧数据的属性进行统计,在查找的时候根据属性来查找数据的位置,而不是根据数据的名字来查找,这种索引的每一项都包括一个属性值和具有该属性值的各数据的位置,由属性值确定记录的位置,因此称为倒排索引。

一般一些搜索引擎都是基于倒排索引实现的。

二叉查找树

  又叫二叉排序树,如果它不是空树,则它所有结点的左子结点都会比这个结点小,右子结点都会比这个结点大。

构造二叉查找的目的,是为了提高查找和插入删除数据的速度(数据在插入或删除的时候不需要移动元素,只需要找到位置之后修改连接的指针就行了),所以二叉查找树更适用于动态查找。

  然而,二叉查找树的查找性能取决于树的深度,但是对于同样的数据,插入顺序不同,它的深度有可能是不同的,所以它的性能是不稳定的。所以才有了后面基于它的改善结构。

平衡二叉树(AVL树)

  平衡二叉树是一种严格平衡的二叉查找树,它如果非空,则有以下性质:
- 左右子树深度之差的绝对值不超过1;
- 左右子树仍然为平衡二叉树.

平衡二叉树具有一个平衡因子BF,等于左子树深度减去右子树深度,构建平衡二叉树和每次插入、删除操作的时候都要进行调整,使树的BF等于-1、0或者1,从而保证二叉查找树的查找效率。但是平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。

红黑树(RBT树)

  红黑树是弱平衡的二叉查找树,具有以下性质
1. 结点是黑色或者是红色的。
2. 根结点必须是黑色的。
3. 每个叶子结点,即空结点(NIL)是黑色的。
4. 红色结点的两个儿子都是黑色的。
5. 对每个结点,从该结点到其每个叶子结点的所有简单路径都包含相同数目的黑色结点。

红黑树追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。

多路查找树(B树)

  前面讲到的树查找算法,都是典型的二叉树结构,只是在内存里面查找是很不错,但是应用在外存查找中,就有可能会因为树的深度过深而导致IO读取过于频繁,而导致效率低下
  多路查找数就是每一个结点可以多于两个,而且每一个结点处可以存储多个元素的树。这样增加的存储结点的个数,自然就降低了树的深度。

2-3树

  2-3树是每一个结点都具有两个孩子(2结点)或者三个孩子(3结点)的树。而且:

  • 一个2结点包括一个元素和三个孩子,或者没有孩子。
  • 一个3结点包括一小一大两个元素和三个孩子,或者没有孩子。
2-3-4树

  2-3-4树是2-3树的扩展,每个节点都具有2个孩子(2节点)或者3个孩子(3节点)或者4个孩子(4节点)。

  • 一个2节点包含一个元素和两个孩子(或没有孩子)。
  • 一个3节点包含两个元素和三个孩子(或没有孩子)。
  • 一个4节点包含三个元素和四个孩子(或没有孩子)。
      
B树

  B树是一种平衡的多路查找树,2-3树(3阶B树)和2-3-4树(4阶B树)都是B树的特例。一个m阶的B树具有如下性质:

  • 如果根节点不是叶子节点,则其子树个数范围为[2,m]。
  • 分支节点子树范个数围为[m/2,m]。
  • 所有叶子节点位于同一层次。
  • 所有分支节点包含下列信息:(n,A0,K1,A1,K2,A2,···,Kn,An),其中n是关键字的个数,K1到K2就是那n个关键字,A0到An是指向子树根结点的指针。
B+树

  对于树结构来说,我们都可以通过中序遍历来顺序查找树中的元素,这一切都是在内存中进行。可是B树结构中,我们往返于每个节点之间也就意味着,我们必须在硬盘的页面之间进行多次访问。
  为了能够解决所有元素遍历等基本问题,我们在原有B树结构基础上,加上了新的元素组织方式,这就是B+树。它和B树不同的地方有:
- 有n棵子树的节点中包含n个关键字。
- 所有的叶子节点包含全部关键信息,及指向含这些关键字记录的指针。
- 所有分支结点可以看成是索引,节点中仅含有其子树中的最大(或最小)关键字。

这样的话,如果我们是要从最小关键字从小到大进行顺序查找,我们就可以从最左侧的叶子节点出发,不经过分支节点,而是沿着指向下一个叶子节点的指针遍历所有的关键字。

散列表(哈希表)

  之前的查找方法都是通过比较来找到数据的,而哈希表就是直接通过关键字来找到数据的:散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,是的每个关键字key对应一个存储位置f(key),根据这样的映射关系,我们只需要提供关键字就可以很快地找到数据的存储位置,取出数据。

优缺点

  优点

  • 简化了比较过程,插入操作和查找操作的效率大大提高,最适合查找与给定值相等的记录。

      缺点

  • 不适合一个关键字对应多个记录的情况。

  • 因为没有比较,不适合范围查找。
  • 会有冲突现象:对不同的关键字可能得到同一哈希地址。
散列函数构造方法

  散列函数就是上面说到的f(key)里面的f(),在现实中,我们应该根据不同的情况采用不同的散列函数,可以根据以下的因素进行考虑:
1. 计算散列地址所需要的时间。
2. 关键字的长度
3. 散列表的大小
4. 关键字的分布情况
5. 记录查找的频率。

直接定址法

  取关键字的某个线性函数值为散列地址:f(key) = a * key +b
  这样的散列函数简单,均匀,不会产生冲突,但是需要实现知道关键字分布情况,适合查找表较小而且连续的情况。

数字分析法

  抽取关键字的一部分来计算散列存储位置的方法。适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑这个方法。

平方取中法

  将数字平方然后取结果中间几位数的函数。比较适合不知道关键字的分布,而位数又不是很大的情况。

折叠法

  将关键字从左到右分割成几部分,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。这种实现不需要知道关键字的分布,适合关键字位数较多的情况。

除留余数法

  这是最常用的构造散列函数方法,对于散列表长为m的散列函数公式为:f(key) = key mod p (p<=m)

随机数法

  选择一个随机数,取关键字的随机函数值作为它的散列地址:f(key) = random(key)

处理散列冲突的方法
开放定级法

  一旦发生了冲突,就与寻找下一个空的散列地址,只要列表足够大,空的散列地址总能找到,并将记录存入。

再散列函数法

  对于有冲突的关键字,再准备一个散列函数对它进行求散列地址,这种方法能使关键字不产生聚集,相应的也增加了计算的时间。

链地址法

  将所有关键字为同义词的记录存储在一个单链表中,如果有冲突,就在单链表增加一个结点存储。这样有冲突不用换地方,能够解决多个冲突,但是这样耗费的存储空间就比较大了。

参考

  《大话数据结构》 —— 程杰

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值