算法与数据结构 — 结合 Java 源码分析数据结构

算法与数据结构 — 结合 Java 源码分析数据结构

本博客为基本知识点的浅尝辄止,仅供参考,详细的需要读者继续查阅资料和实践。
建议读者在学习这些基础的数据结构时,能够 参照编程语言里面的源码,效果会更佳。

一、概念

  • 在计算机科学中,数据结构(英语:data structure)是计算机中存储、组织数据的方式。

  • 数据结构意味着接口或封装:一个数据结构可被视为两个函数之间的接口,或者是由数据类型联合组成的存储内容的访问方法封装。

  • 不同种类的数据结构适合不同种类的应用,部分数据结构甚至是为了解决特定问题而设计出来的。例如B树即为加快树状结构访问速度而设计的数据结构,常被应用在数据库和文件系统上。

  • 系统架构的关键因素是数据结构而非算法的见解,导致了多种形式化的设计方法与编程语言的出现。绝大多数的语言都带有某种程度上的模块化思想,透过将数据结构的具体实现封装隐藏于用户界面之后的方法,来让不同的应用程序能够安全地重用这些数据结构。C++、Java、Python等面向对象的编程语言可使用类 (计算机科学)来达到这个目的。

  • 因为数据结构概念的普及,现代编程语言及其API中都包含了多种默认的数据结构,例如 C++ 标准模板库中的容器、Java集合框架以及微软的.NET Framework。

二、常见的数据结构

  • 线性表
    • 顺序表 - 数组(Array)
    • 链表(Linked List)
  • 栈(Stack)
  • 队列(Queue)
  • 树(Tree)
  • 图(Graph)
  • 堆(Heap)
  • 散列表(Hash table)

1. 数组(Array)- 顺序表

  1. 概念:
    在计算机科学中,数组数据结构(array data structure),简称数组(Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的存储地址。

    • 一维数组:

      最简单的数据结构类型是一维数组。例如,索引为0到9的40位整数数组,可作为在存储器地址2000,2004,2008,…2040中,存储10个变量,因此索引为 i 的元素即在存储器中的 2000 + 4×i 地址。数组第一个元素的存储器地址称为第一地址或基础地址。

      int[] array = new int[10];
      
    • 二维数组:

      二维数组,对应于数学上的矩阵概念,可表示为二维矩形格。

      int[][] b = {{3, 6, 2}, {0, 1, -4}, {2, -1, 0}};
      // 或
      int[][] b = new int[][]{{3, 6, 2}, {0, 1, -4}, {2, -1, 0}};
      

2. 链表(Linked List)

  1. 概念
    链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按顺序表的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。

    由于不必须按顺序存储,链表在插入的时候可以达到 O(1) 的复杂度,比顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(n)和O(1)。

    使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

    链表中最简单的一种是单向链表,它包含两个域,一个数据域和一个指针域。这个指针域指向列表中的下一个节点,而最后一个节点则指向一个空值。

    在这里插入图片描述

  2. 代码使用:

    // LinkedList是双向链表
    List<Integer> linkList = new LinkedList<>();
    

    根据jdk中的源码,左边是 LinkedList 提供的函数API,右边箭头处可以看到 LinkedList 是双向链表。
    在这里插入图片描述


3. 栈(Stack)

  1. 概念:

    堆栈(stack)又称为栈或堆叠,是计算机科学中的一种抽象数据类型。

    只允许在有序的线性数据集合的一端(称为栈顶端,top)进行加入数据(push)和移除数据(pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作。

    常与另一种有序的线性数据集合队列相提并论。

    堆栈常用一维数组或链表来实现。

    在这里插入图片描述

  2. 操作:
    堆栈使用两种基本操作:推入(压栈,push)和弹出(出栈,pop):
    压栈:将数据放入堆栈顶端,堆栈顶端移到新放入的数据。
    出栈:将堆栈顶端数据移除,堆栈顶端移到移除后的下一笔数据。

  3. 特点:
    堆栈的基本特点:
    先入后出,后入先出。
    除头尾节点之外,每个元素有一个前驱,一个后继。

  4. 堆栈的应用:
    回溯
    递归
    深度优先搜索

  5. 代码使用:

    Stack stack = new Stack();
    
    1. 创建栈:			public Stack() { }
    2. 出栈:			public E push(E item) { }
    3. 入栈:			public synchronized E pop() { }
    4. 查看栈顶元素:		public synchronized E peek() { }
    5. 查找元素在栈中位置:public synchronized int search(Object o) { }
    

    在这里插入图片描述


4. 队列(Queue)

  1. 概念
    队列,是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。
    队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。

  2. 使用场景:
    因为队列先进先出的特点,在多线程阻塞队列管理中非常适用。

  3. 代码使用:
    Java 集合中的 Queue 接口继承自 Collection 接口 ,Deque, AbstractQueue,CheckedQueue,AsLIFOQueue,BlockingQueue,ConcurrentLinkedQueue 等类(接口)都实现或继承了它。
    在这里插入图片描述基础的 Queue接口,声明了如下这些方法:

    在这里插入图片描述

    • add
    • offer
    • remove
    • poll
    • element
    • peek
    // LinkedList是双向链表,它实现了Dequeue接口
    Queue<Integer> queue = new LinkedList<>();
    

5. 树(Tree)

  1. 概念:
    在计算机科学中,树(tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

    1)每个节点都只有有限个子节点或无子节点;

    2)没有父节点的节点称为根节点;

    3)每一个非根节点有且只有一个父节点;

    4)除了根节点外,每个子节点可以分为多个不相交的子树;

    5)树里面没有环路(cycle)


6. 图(Graph)

7. 堆(Heap)


8. 散列表(Hash table)

  1. 概念:

    • 散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

    • 若关键字为 k,则其值存放在 f(k) 的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系 f 为散列函数,按这个思想建立的表为散列表。

    • 对不同的关键字可能得到同一散列地址,即 k1 != k2,而 f(k1)=f(k2),这种现象称为 冲 突 \color{red}冲突 (Collision)。具有相同函数值的关键字对该散列函数来说称做 同 义 词 \color{red}同义词 。综上所述,根据散列函数 f(k) 和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址。

    • 若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。

  2. 构造散列函数:
    散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快定位。方法如下:

    • 直接定址法:取关键字或关键字的某个线性函数值为散列地址。即 hash(k) = k 或 hash(k) = a * k + b,其中a, b为常数(这种散列函数叫做自身函数)
    • 数字分析法:假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
    • 平方取中法:取关键字平方后的中间几位为哈希地址。通常在选定哈希函数时不一定能知道关键字的全部情况,取其中的哪几位也不一定合适,而一个数平方后的中间几位数和数的每一位都相关,由此使随机分布的关键字得到的哈希地址也是随机的。取的位数由表长决定。
    • 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。
      随机数法
    • 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 hash(k)=k mod p, p <= m。不仅可以对关键字直接取模,也可在折叠法、平方取中法等运算之后取模。对p的选择很重要,一般取素数或m,若p选择不好,容易产生冲突。
  3. 处理冲突:
    为了知道冲突产生的相同散列函数地址所对应的关键字,如当f(k1) = f(k2)时,如何确认关键字为 k1 还是 k2 ,必须选用另外的散列函数,或者对冲突结果进行处理。而不发生冲突的可能性是非常之小的,所以通常对冲突进行处理。常用方法有以下几种:

    • 开放定址法(open addressing):hashi = (hash(key) + di) mod m, i = 1,2…k(k <= m - 1) 其中hash(key)为散列函数,m为散列表长,di 为增量序列,i为已发生冲突的次数。增量序列可有下列取法:

      • di = 1,2,3…(m-1)称为 线性探测(Linear Probing);即di = i,或者为其他线性函数。相当于逐个探测存放地址的表,直到查找到一个空单元,把散列地址存放在该空单元。
      • di = ±12,±22,…,±k2称为 平方探测(Quadratic Probing)。相对线性探测,相当于发生冲突时探测间隔 di = i2 个单元的位置是否为空,如果为空,将地址存放进去。
      • di = 伪随机数序列,称为 伪随机探测。
    • 单独链表法:将散列到同一个存储位置的所有元素保存在一个链表中。实现时,一种策略是散列表同一位置的所有冲突结果都是用栈存放的,新元素被插入到表的前端还是后端完全取决于怎样方便。

    • 双散列。

    • 再散列:hashi = hashi(key), i=1,2…k。hashi 是一些散列函数。即在上次散列计算发生冲突时,利用该次冲突的散列函数地址产生新的散列函数地址,直到冲突不再发生。这种方法不易产生“聚集”(Cluster),但增加了计算时间。

    • 建立一个公共溢出区

      举例:
      显示线性探测填装一个散列表的过程:
      关键字为{89,18,49,58,69}插入到一个散列表中的情况。此时线性探测的方法是取{\displaystyle d_{i}=i}d_{i}=i。并假定取关键字除以10的余数为散列函数法则。
      散列地址 空表 插入89 插入18 插入49 插入58 插入69
      0 49 49 49
      1 58 58
      2 69
      3
      4
      5
      6
      7
      8 18 18 18 18
      9 89 89 89 89 89
      第一次冲突发生在填装49的时候。地址为9的单元已经填装了89这个关键字,所以取{\displaystyle i=1}i=1,往下查找一个单位,发现为空,所以将49填装在地址为0的空单元。第二次冲突则发生在58上,取{\displaystyle i=3}{\displaystyle i=3},往下查找3个单位,将58填装在地址为1的空单元。69同理。
      表的大小选取至关重要,此处选取10作为大小,发生冲突的几率就比选择质数11作为大小的可能性大。越是质数,mod取余就越可能均匀分布在表的各处。
      聚集(Cluster,也翻译做“堆积”)的意思是,在函数地址的表中,散列函数的结果不均匀地占据表的单元,形成区块,造成线性探测产生一次聚集(primary clustering)和平方探测的二次聚集(secondary clustering),散列到区块中的任何关键字需要查找多次试选单元才能插入表中,解决冲突,造成时间浪费。对于开放定址法,聚集会造成性能的灾难性损失,是必须避免的。

  4. 代码使用:

    Map<String, Integer> hashtable = new Hashtable<>();
    Map<String, Integer> hashMap = new HashMap<>();
    

三、参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值