数构与算法:数据结构

一、说明        

        数据结构,简单来说,是存储、组织数据的方式。它是指相互之间存在一种或多种特定关系的数据元素的集合。

        换句话说,数据结构试图在最基本的语言类型(如C语言中的int、float等)上构建更加复杂的“基本类型”,可以说是复合类型,这样,数据将可以以一种更加逻辑化的方式进行存储和操作。而关于如何定义元素,元素之间的关系和元素操作被概括进了一个叫做ADT(Abstract Data Type)的概念中。它的一般形式如下:

        这是一个绝佳,基本的定义,因为即使是已存在的最基本数据类型,如int也可以经此划分,这便是抽象的概括作用。

        基于此定义,不同编程语言的具体实现是不同的,一般而言,数据对象(在面向对象的编程语言中,通过类表示,而像C语言这样的语言,就C而言通过结构体表示);基本操作,在任何编程语言中都是通过方法/函数;数据关系,通过给数据对象添加额外的指向属性表示,如,在C语言种,可以通过在数据对象结构体中添加成员指针指向另一个数据对象。一般而言,会存在一个统一的容器用于管理各个数据对象,所以数据对象本身的额外指向属性可以通过容器体现。

        从某种层面来说,C提供了水到渠成的数据结构设计因素,一方面,C语言提供的数据结构(几乎没有)很少,另一方面,在C语言中定义数据结构也很容易体悟数据结构本身的巧妙之处。

        值得说明的是,数据结构并不经常创建新的数据结构,而是认识数据结构,上面的ADT仅仅是作为统一那些常见数据结构的一个通式和解构途径。同时,虽然已经有成型的所谓的常见数据结构,但是其具体实现是因人而异的,就比如一个线性表,以下两种未尝不是思路。

typedef struct SeqList{

    size_t size;
    int * data;
}SeqList;

typedef struct SeqList{

    size_t size;
    int data[];
}SeqList;

        最后,数据结构尽管是语言独立的,但是对于有些高级语言,已经实现了一些数据结构并纳入了自己的语法体系。所以在这个层面上而言,数据结构是拿来用的,不过如果有人想要自己设计也未尝不可,毕竟C语言就没有。

        本系列笔记,不谈数据结构的实现,因为这很无聊(有时候),你无法保证自己设计的数据结构很好,具有足够的容错性,再者以后你用到其他人写的数据结构可能会主观代入,认为它就是怎么样设计的,这点会有糟糕(如果你猜对了,也没多好),但不管怎么样,这取决于个人,拿这练练编程也未尝不可。

二、基本概念

(一)数据、数据元素与数据项

数据:数据是对信息的一种符号表示,是计算机中可以操作的对象,能被计算机识别并处理。它不仅包括整数、实数等数值类型,还涵盖字符、声音、图像、视频等非数值类型。例如,图像文件中的像素值就是一种数据。

数据元素:是组成数据的、有一定意义的基本单位,在计算机中常作为一个整体处理,也被称为记录。比如在学生成绩表中,每一行记录一个学生的各项成绩信息,这一行就可视为一个数据元素。

数据项:一个数据元素可由若干个数据项组成,数据项是不可分割的最小单位。仍以学生成绩表为例,学生的姓名、学号、各科成绩等都是数据项。

数据、数据元素和数据项的关系为:数据由多个数据元素组成,而每个数据元素又由若干个数据项构成。

源: 数据、数据元素、数据项、数据对象的介绍和理解_数据项是什么-CSDN博客

在实际应用中,比如图书馆的图书管理系统,每一本图书的信息(如书名、作者、出版年份、ISBN 号等)可以看作一个数据元素,其中书名、作者等就是数据项。

(二)数据的逻辑结构

        所谓逻辑结构,其实就是关系结构,各个数据元素之间的联系。

集合结构:集合中的数据元素除了同属于一种类型外,彼此之间没有其他明确的关系,组织形式松散。例如,一堆不同颜色的球组成的集合。

线性结构:元素之间存在一对一的关系。常见的如数组、链表、队列、栈等。以链表为例,每个节点都有且仅有一个指向下一节点的指针,形成了一对一的顺序关系。

树形结构:元素之间存在一对多的关系。比如二叉树,每个节点最多有两个子节点,父节点与子节点之间呈现出一对多的关系。

图结构:元素之间存在多对多的关系。例如社交网络中,用户之间可以有多种复杂的关联。

源: 数据结构-第一课_数据结构第一颗-CSDN博客

        集合结构相对松散,线性结构具有明确的顺序性,树形结构体现了层次分明的分支特性,图结构则展现了复杂的多向连接。

(三)数据的存储结构

        存储结构是指数据的物理存储方式,最基本的是前面两种,后面两种从某种程度上而言是前面两种的结合

顺序存储:顺序存储一般通过数组实现,因为数组分配的就是连续的内存地址。采用顺序存储可以很方便的定位元素,但是对于C语言,由于数组长度的限制,这样的方式对于元素的个数就很敏感了。缺点是插入和删除操作需要移动大量元素,效率较低。

链式存储:数据元素的存储位置不连续,通过指针来表示元素之间的逻辑关系。优点是插入和删除操作灵活,无需移动大量元素;缺点是查找效率较低,且每个元素因存储指针而占用额外空间。常用于频繁插入和删除操作的情况,如链表。

源: 线性表的链式存储结构(十) - 软件技术 - 亿速云

索引存储:在存储数据元素的同时,还建立附加的索引表。就类似字典的目录一样,这可以通过不同的或者多维数组实现,也可以通过数组和指针实现。其优点是检索速度快,能保证数据的唯一性;缺点是创建和维护索引需要时间和空间,数据增删改时也要维护索引。适用于对检索速度要求高且数据量较大的场景。

源: 索引存储结构和分块(索引)查找_索引存储结构举例-CSDN博客

散列存储:根据数据元素的关键字直接计算出存储地址。优点是检索、增加和删除操作都很快;缺点是好的散列函数较难设计,可能出现冲突,解决冲突会增加时间和空间开销。常用于快速查找和存储的场合,如哈希表。

源: 数据结构:哈希表_哈希表的键必须是整数吗-CSDN博客

二、线性结构

(一)线性表

        线性表是数据结构中最基本的一种数据结构。它是由n个具有相同数据类型的数据元素构成的有限序列。其中,n表示线性表中元素的个数,n>=0。线性表中的数据元素可以是任意类型的,如整数、字符、实数等。

C语言中,线性表可以通过结构体来定义。一个基本的线性表结构可以包含以下几个元素:

  1. 数据元素:线性表中的各个元素的实际数据。
  2. 头指针:指向线性表的第一个元素的指针。
  3. 长度:线性表中元素的个数。

下面是一个基本的线性表结构的定义:

// 定义线性表的数据类型
typedef struct {
    int data[MAX_SIZE];  // 线性表的数据元素
    int length;  // 线性表的长度
} List;

        在这个定义中,MAX_SIZE 是一个常量,表示线性表的最大容量,可以根据实际需求进行调整。可以通过访问 List 结构体的 datalength 成员来操作线性表的数据。

线性表的ADT可以包括以下操作:

  1. 创建线性表:创建一个空的线性表。
  2. 销毁线性表:销毁线性表,并释放相关资源。
  3. 清空线性表:清空线性表中的所有元素。
  4. 判断线性表是否为空:判断线性表中是否没有元素。
  5. 获取线性表的长度:返回线性表中元素的个数。
  6. 获取线性表中索引为i的元素:返回线性表中索引为i的元素的值。
  7. 在指定位置插入元素:在线性表的指定位置插入一个元素。
  8. 删除指定位置的元素:删除线性表中指定位置的元素。
  9. 查找指定元素的位置:返回线性表中第一个与指定元素相等的元素的位置。

(二)栈(Stack)

        栈(Stack)是一种先进后出(Last In, First Out,LIFO)的数据结构,类似于我们平时使用的一摞盘子。栈的操作只在一端进行,该端被称为栈顶,相对的另一端被称为栈底。

源: 数据结构——栈,队列,及其结构特点应用。

        栈可以理解为是一种操作受限的线性表,所以它的定义,和线性表可以是一样的,以下为一种参考定义:

#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];
    int top;
} Stack;

        栈支持两种基本操作:push和pop。push操作将一个元素添加到栈顶,而pop操作将栈顶的元素移除并返回。

栈的基本特性如下:

  1. 入栈(push):将元素添加到栈顶,即创建一个新的元素并将其放置在上一个栈顶元素之上。
  2. 出栈(pop):将栈顶的元素移除并返回该元素的值。
  3. 栈顶(top):获取栈顶元素的值,不改变栈的状态。
  4. 栈的大小(size):获取栈中元素的个数。
  5. 判空(empty):判断栈是否为空,即栈中是否有元素。

        栈的应用包括但不限于以下场景:

  1. 表达式求值:在编译器和解释器中,栈常用于计算表达式的值。比如,对于一个数学表达式,我们可以将其转换为后缀表达式(逆波兰表达式),然后使用栈来求解。
  2. 函数调用:在函数调用时,栈用于存储函数的局部变量、参数和返回地址等信息。每次函数调用时,栈顶指针会指向函数的栈帧,函数执行完毕后,栈帧会被弹出。
  3. 括号匹配:栈可以用于判断括号是否匹配。遍历输入字符串,当遇到左括号时,将其入栈,当遇到右括号时,判断栈顶元素是否为相应的左括号,如果是,则出栈,否则表示括号不匹配。
  4. 浏览器的前进后退功能:浏览器中的前进和后退功能可以使用两个栈来实现,一个栈用于存储已访问的页面,另一个栈用于存储后退的页面。

        在实现栈时,可以使用数组或链表来存储元素。数组实现的栈称为顺序栈,链表实现的栈称为链式栈。具体的实现细节可能因编程语言和需求而有所不同。

(三)队列(Queue)

        队列是一种先进先出(FIFO)的数据结构。它可以理解为一种特殊的线性表,只能在表的一端(称为队尾)进行插入操作,在另一端(称为队首)进行删除操作。队列在生活中的例子有排队买票、排队上车等。

源自: https://zhuanlan.zhihu.com/p/361138097?just_published=2
typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
} Queue;

队列有两个基本操作:

  1. 入队(enqueue):将元素插入队尾。
  2. 出队(dequeue):将队首的元素删除并返回。

其他常见的队列操作还包括:

  1. 获取队首元素(front):返回队首元素,但不删除。
  2. 判断队列是否为空(isEmpty):若队列为空,返回 true;否则返回 false。
  3. 获取队列长度(size):返回队列中元素的个数。

        队列可以用数组或链表实现。使用数组实现的队列称为顺序队列,使用链表实现的队列称为链式队列。

        队列的应用场景很多,比如CPU调度、打印任务队列、消息队列等。在算法中,队列常被用于广度优先搜索(BFS)和树的层次遍历等问题的解决。

        需要注意的是,队列的大小有限制,当队列已满时无法入队;当队列为空时无法出队。这种限制可以通过循环队列解决,即利用数组的循环,将队列首尾相接。

(四)字符串(String)

        虽然字符串在某些上下文中被视为原始数据类型(如C语言中的字符数组),但在许多高级编程语言中,字符串被视为一种特殊的数据结构。

        串的匹配和搜索是串处理中两个非常重要的操作。

        串的匹配是指在一个给定的主串中寻找某个子串的过程。主串是一个较长的字符串,而子串是主串中的一个较短的字符串。匹配的目的是找到子串在主串中的位置或确定子串是否存在于主串中。匹配算法的常见方法包括暴力匹配算法、KMP算法、Boyer-Moore算法等。匹配操作在很多应用中非常常见,比如字符串搜索、文本编辑器中的查找替换等。

        串的搜索是指在一组给定的串中寻找满足某种条件的串的过程。搜索的目的是找到满足条件的串的位置或确定这些串的存在性。搜索算法的常见方法包括暴力搜索算法、基于哈希的搜索算法、基于前缀树的搜索算法等。搜索操作在很多应用中非常常见,比如拼写检查、自动补全、信息检索等。

        匹配和搜索是串处理中的基本操作,它们在很多领域中都有广泛的应用。对于大规模的数据处理和文本处理,高效的匹配和搜索算法可以大大提高处理速度和效率,因此它们一直是研究和优化的热点。

(五)列表(List)

        列表,在有些书籍中,也叫广义表,在数据结构中,列表是一种有序的数据集合,可以存储多个元素。列表中的元素可以是任意类型的数据,比如整数、浮点数、字符串等。

列表的特点包括:

  1. 有序性:列表中的元素按照一定的顺序存储,每个元素都有一个索引,可以通过索引来访问和操作元素。
  2. 可变性:列表的长度和内容是可变的,可以动态地添加、删除和修改元素。
  3. 元素的存储方式:列表使用一块连续的内存空间来存储元素,元素之间的内存地址是相邻的。

列表常用的操作包括:

  1. 访问和修改元素:可以使用索引来访问列表中的任意元素,也可以通过索引来修改元素的值。
  2. 添加元素:可以在列表的末尾添加一个元素,也可以在指定的位置插入一个元素。
  3. 删除元素:可以删除列表中指定位置的元素,也可以根据元素的值来删除元素。
  4. 切片操作:可以通过切片操作来获取列表中的一部分元素。
  5. 长度和容量:可以获取列表的长度,即列表中元素的个数,也可以获取列表的容量,即列表当前的存储空间大小。

列表在实际应用中非常常见,可以用来存储一组数据,比如学生姓名、成绩等。列表还可以用来实现其他数据结构,比如栈、队列和链表等。在编程语言中,列表通常都有相应的内置数据结构和操作,使得使用更加方便和高效。

三、树型结构

(一)树的基本概念

        树是由 n 个结点组成的有限集合,当 n=0 时为空树。非空树中仅有一个根结点,其余结点形成多个互不相交的子集,称为根的子树。森林是由 m 棵互不相交的树组成的集合。在树中,具有相同双亲的结点称为兄弟节点,而兄弟节点中的同性节点可称为姐妹节点。

(二)二叉树的基本概念

        在二叉树中,度指的是结点拥有的子树数量。层级从根结点开始计算,根为第 1 层,其孩子为第 2 层,依次类推。叶子结点是度为 0 的结点,没有子树。根是二叉树的起始结点。满二叉树是除了叶子结点外,每一个结点都有左右子叶且叶子结点都在同一层的二叉树。完全二叉树是除了最后一层,其他各层的结点数都达到最大,且最后一层的结点集中在左侧。

(三)二叉树的基本操作

        二叉树的常见操作包括遍历。先序遍历先访问根结点,再遍历左子树和右子树;中序遍历先遍历左子树,再访问根结点,最后遍历右子树;后序遍历先遍历左子树和右子树,最后访问根结点。此外,树与森林的相互转换也很重要。将树转换为二叉树时,要遵循特定的加线、抹线和旋转规则。森林转换为二叉树则是先将每棵树转换为二叉树,再依次连接各棵树的根结点。

(四)不同树

1、BST(二叉搜索树)

BST(二叉搜索树)是一种特殊类型的二叉树,它满足以下性质:

  1. 对于任意节点node,其左子树中的所有节点的值都小于node的值。
  2. 对于任意节点node,其右子树中的所有节点的值都大于node的值。
  3. 左右子树也都是BST。
源自: https://zhuanlan.zhihu.com/p/139957327

2、AVL (平衡树二叉树)

        AVL树是一种自平衡二叉搜索树,它在每个节点上维护一个平衡因子(Balance Factor),用于保持树的平衡。

        平衡因子是指左子树的高度减去右子树的高度的差距。AVL树的平衡因子可能是-1、0或1,如果平衡因子超出这个范围,就需要进行旋转操作来重新平衡树。

AVL树的特点如下:

  1. 对于任意节点,它的左子树和右子树的高度差不超过1;
  2. 每个节点的左子树和右子树也都是AVL树。

AVL树的插入和删除操作都会涉及到平衡因子的调整和可能的旋转操作。插入操作的基本步骤如下:

  1. 将新节点插入到AVL树中的合适位置;
  2. 沿着插入路径向上,更新每个节点的平衡因子;
  3. 如果某个节点的平衡因子超出了范围,进行相应的旋转操作来重新平衡树。

旋转操作可以分为四种:左旋、右旋、左右旋和右左旋。它们的目的是为了调整树的结构,使得树保持平衡。

AVL树的删除操作也类似,但是需要考虑更多的情况。删除操作的基本步骤如下:

  1. 找到要删除的节点;
  2. 根据节点的子节点情况,分情况进行删除操作;
  3. 沿着删除路径向上,更新每个节点的平衡因子;
  4. 如果某个节点的平衡因子超出了范围,进行相应的旋转操作来重新平衡树。
图源: https://blog.51cto.com/u_15352924/3743140

3、红黑树

红黑树是一种自平衡的二叉搜索树,它的结构具有以下特点:

  1. 每个节点要么为红色,要么为黑色。
  2. 根节点必须为黑色。
  3. 所有叶子节点(NIL节点)为黑色。
  4. 如果一个节点为红色,则它的两个子节点必须为黑色。
  5. 任意节点到其每个叶子节点的路径都包含相同数目的黑色节点。

红黑树保持了其平衡性质,这使得其插入、删除和搜索操作的时间复杂度保持在O(logN)级别。红黑树的平衡性通过以下规则来确保:

  1. 保持黑色节点的平衡:对于任意节点,其左子树和右子树的黑色节点数目相等或相差不超过1。
  2. 保持红色节点的相对平衡:红色节点不能连续出现,即红色节点的父节点和子节点不能同时为红色。

红黑树的插入操作:

  • 当插入节点时,首先按照二叉搜索树的规则将节点插入到适当的位置。
  • 将插入的节点标记为红色,然后通过一系列的旋转和颜色调整来保持红黑树的性质。
  • 如果插入的节点的父节点为黑色,或者插入节点的父节点为红色但是其父节点的兄弟节点为黑色,那么红黑树的性质就不会被破坏,无需进行额外的操作。
  • 如果插入的节点的父节点为红色并且其父节点的兄弟节点也为红色,那么需要进行旋转和颜色调整来恢复红黑树的性质。

红黑树的删除操作:

  • 当删除节点时,首先按照二叉搜索树的规则找到要删除的节点。
  • 如果要删除的节点有两个子节点,那么需要找到其后继节点(即右子树中的最小节点)来替代要删除的节点。
  • 如果要删除的节点只有一个子节点或者没有子节点,那么可以直接删除。
  • 删除节点后,通过一系列的旋转和颜色调整来保持红黑树的性质。

        红黑树通过保持节点的颜色和位置来维持平衡,使得红黑树的高度始终保持在O(logN)级别,从而保证了插入、删除和搜索操作的高效性能。由于红黑树的插入和删除操作较为复杂,实现上相对较为复杂,但是其高效的性能使其成为了一种常用的数据结构,被广泛应用于各种场景中。

4、扩充二叉树

        扩充二叉树是一种特殊的二叉树,它是由原始二叉树通过添加特殊节点而生成的。在扩充二叉树中,每个原始节点都被分为两个节点,一个是原始节点本身,另一个是一个空节点。空节点在扩充二叉树中表示一个虚拟节点,它的存在是为了保持二叉树的结构完整性。

扩充二叉树可以通过递归的方式来构建。具体的构建过程如下:

  1. 如果原始二叉树为空,那么扩充二叉树也为空。

  2. 否则,创建一个新节点,值为原始节点的值,将其作为扩充二叉树的根节点。

  3. 在新节点的左子树中递归构建原始节点的左子树的扩充二叉树。

  4. 在新节点的右子树中递归构建原始节点的右子树的扩充二叉树。

  5. 返回新节点作为扩充二叉树的根节点。

        通过这个构建过程,原始二叉树的每个节点都会被扩充为两个节点,其中一个是原始节点本身,另一个是一个空节点。扩充二叉树的叶子节点都是空节点,它们的存在是为了保持二叉树的结构完整性。

        扩充二叉树在一些算法中经常被用到,比如二叉树的层次遍历、序列化和反序列化等操作。它可以方便地表示一个完整的二叉树结构,同时也可以在一些操作中占位,用于标记特定的位置。

        需要注意的是,扩充二叉树并不是一棵真正的二叉树,它只是原始二叉树的一种扩展形式。在一些操作中,可能需要对空节点进行特殊处理。

5、线索二叉树

        线索二叉树是一种改进的二叉树数据结构,其目的是在不增加额外空间的情况下,提高二叉树的遍历效率。

        在普通二叉树中,遍历需要通过递归或者使用栈来实现。而在线索二叉树中,每个节点都存储了指向前驱节点和后继节点的指针,从而可以在不使用递归或栈的情况下完成遍历。

        线索二叉树分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种形式,根据线索的位置不同。

        线索二叉树的建立需要进行遍历,并通过递归或者栈的方式来实现。在遍历的过程中,根据节点的前驱或后继节点是否存在来判断是否需要修改指针,从而连接线索。

        线索二叉树的优点是可以直接通过前驱或后继指针进行遍历,不需要递归或者栈的辅助。在实际应用中可以提高二叉树的遍历效率。缺点是在建立线索二叉树时需要额外的时间和空间开销。

6、B树和B+树

        B树和B+树是一种常用的数据结构,用于在磁盘存储中高效地组织和访问大量的数据。

        B树是一种平衡搜索树,它的特点是每个节点可以有多个子节点。B树的每个节点都包含一些关键字,用于索引和排序数据。B树的节点分为内部节点和叶子节点,内部节点只包含关键字和指向子节点的指针,而叶子节点还包含实际的数据。B树的关键字按照从小到大的顺序排列,这使得在搜索和插入操作时能够快速定位到目标位置。B树的高度较低,因此访问数据的效率较高。

        B+树是在B树的基础上进行了一些改进。B+树与B树的区别在于,B+树的叶子节点不包含实际的数据,而只包含关键字和指向数据的指针。这样可以使得B+树的内部节点能够存储更多的关键字,从而减少了树的高度,提高了查询效率。另外,B+树的叶子节点之间通过指针连接起来,形成一个有序的链表,这样可以快速地进行范围查询。B+树广泛应用于数据库系统中的索引结构,能够支持高效的范围查询和顺序访问。

        总的来说,B树适合于随机读写的情况,而B+树适合于范围查询和顺序访问的情况。两者在实际应用中都有广泛的应用,用于提高数据的存储和访问效率。

7、哈夫曼树

        哈夫曼树(Huffman Tree),也称为最优二叉树,是一种特殊的二叉树。它以一种带权路径长度最短的方式来存储数据。哈夫曼树主要应用于数据压缩领域,可以将数据转化为更小的二进制码来减少存储空间。

哈夫曼树的构建过程是从叶子节点开始,根据节点的权值来构建,权值较小的节点离树根较近,权值较大的节点离树根较远。构建哈夫曼树的算法如下:

  1. 创建一个包含所有要存储的数据和权值的节点集合。
  2. 找出权值最小的两个节点作为左右子节点,形成一个新的节点,权值为两个子节点的权值之和。
  3. 将新节点插入到节点集合中,并移除被合并的两个节点。
  4. 重复步骤2和3,直到只剩下一个节点,该节点即为哈夫曼树的根节点。       

         使用哈夫曼树来存储数据时,每个节点的左子节点表示数据的0,右子节点表示数据的1。从根节点到每个叶子节点的路径就是对应数据的二进制编码。因为权值较小的节点离树根较近,所以出现频率高的数据对应的编码较短,出现频率低的数据对应的编码较长,可以实现数据的高效压缩存储。

8、堆

堆(Heap)是一种经过排序的完全二叉树,具有以下两个特点:

  1. 堆结构是一个完全二叉树,即除最后一层外,每一层上的节点数都是满的。
  2. 对于大根堆(Max Heap),每个节点的值都大于等于其子节点的值;对于小根堆(Min Heap),每个节点的值都小于等于其子节点的值。

堆可以使用数组来实现,数组中的每个元素都代表堆中的一个节点。根据数组下标的排列方式,堆可以分为两种类型:

  1. 最大堆(Max Heap):数组中较大的元素在上面,较小的元素在下面。
  2. 最小堆(Min Heap):数组中较小的元素在上面,较大的元素在下面。
图源: 堆简介 – 数据结构和算法教程 | 码农参考

堆常用来实现优先队列,主要是因为堆可以快速找到最大(或最小)的元素,并且在插入和删除元素时的时间复杂度较低。堆的插入和删除操作的时间复杂度都为 O(logN)。

常见的堆操作有:

  1. 插入元素:在堆的最后一个位置插入一个新元素,然后与其父节点进行比较,根据堆的性质调整位置,直到满足堆的要求。
  2. 删除堆顶元素:将堆顶元素与最后一个元素交换位置,然后删除最后一个元素,并将新的堆顶元素与其子节点进行比较,根据堆的性质调整位置,直到满足堆的要求。

堆还有一种常见的应用是堆排序。堆排序的基本思路是首先构建一个堆,然后依次将堆顶元素与最后一个元素交换位置,并将前面的元素重新调整为新的堆,依此类推,直到所有元素都排好序。

9、斐波那契堆

        斐波那契堆是一种优先级队列的数据结构,它基于斐波那契堆树。斐波那契堆是一种松散的、不严格的二项堆,它允许在插入和合并操作中有非常高的效率。

斐波那契堆的特点包括:

  1. 每个节点有多个子节点,但没有指向父节点的指针。这使得在合并两个堆时非常高效。
  2. 每个节点有一个指向同级兄弟节点的指针,这样可以通过指针在同一级别的节点之间进行快速的插入和删除操作。
  3. 堆的根节点形成一个双向循环链表,这样可以快速地找到最小值。

斐波那契堆的具体实现有以下几个主要操作:

  1. 插入:将一个新节点插入到堆中,只需要简单地将新节点添加到根节点的双向循环链表中。
  2. 合并:将两个堆合并成一个堆,只需要简单地将两个堆的根节点的双向循环链表连接在一起。
  3. 删除最小值:找到根节点中的最小值,并将其从堆中删除。删除后,需要将根节点的子节点和兄弟节点重新加入到堆中,并更新最小值。
  4. 减小键值:将某个节点的键值减小,并更新堆中的结构。这个操作涉及到将某个节点剪切并添加到根节点的双向循环链表中。

        斐波那契堆具有较高的插入和合并操作的效率,但删除最小值和减小键值的操作可能会比较慢。总的来说,斐波那契堆适用于那些插入和合并操作频繁,而删除最小值和减小键值操作不频繁的场景。

10、vEB树

        vEB树(van Emde Boas 树)是一种数据结构,用于实现动态集合的字典(dictionary)数据结构。它被设计用来在O(log log u)的时间复杂度下支持插入、删除和查找操作,其中u是存储在vEB树中的元素的最大值。

        vEB树使用一种递归的方式将问题分解为更小的问题。它将一个范围为[0, u-1]的整数集合分成两部分:高位部分(cluster)和低位部分(summary)。高位部分用来存储元素的高位信息,它的大小为√u。低位部分用来递归存储子树,每个子树的大小也为√u。

        在vEB树中,每个元素都会被存储在一个叶子节点中。每个节点都有一个min和max字段,分别表示当前节点所包含的最小和最大元素。另外,每个节点还有一个summary字段,用于存储子树的高位信息。

        vEB树的插入操作可以分为两个步骤。首先,需要判断是否已经存在相同的元素,如果存在,则不进行任何操作。否则,需要更新节点的min和max字段,并递归进行插入操作。

        vEB树的删除操作也分为两个步骤。首先,需要判断是否存在要删除的元素,如果不存在,则不进行任何操作。否则,需要根据元素的高位信息找到对应的子树,并递归进行删除操作。最后,需要更新节点的min和max字段。

        vEB树的查找操作也是递归进行的。首先,需要判断是否存在要查找的元素,如果存在,则返回对应的节点。否则,需要根据元素的高位信息找到对应的子树,并递归进行查找操作。

        vEB树的时间复杂度主要由递归操作的次数决定。在插入、删除和查找操作中,每次递归的规模都是原问题的√u倍。因此,递归操作的次数为O(log log u),从而使得整个操作的时间复杂度为O(log log u)。

四、图型结构

        图(Graph)是一种非线性的数据结构,用于表示对象之间的关系。在图中,节点(也称为顶点)代表对象,边(也称为弧)代表节点之间的连接。图可以用来模拟许多现实世界中的问题,如社交网络中的好友关系、城市间的交通网络等。

(一)图的基本概念

  • 节点(Vertex):图中的一个基本单位,可以代表实体或信息单元。
  • 边(Edge):连接两个节点的线,表示节点之间的关系。
  • 邻接(Adjacency):如果两个节点之间存在一条边,则这两个节点是相邻的。
  • 路径(Path):从一个节点到另一个节点的边序列。
  • 连通性(Connectivity):图中的节点是否可以通过路径相互到达。
  • 环(Cycle):起点和终点相同的路径。

(二)图的分类

  • 有向图(Directed Graph):边有方向,表示从一个节点指向另一个节点的关系。
  • 无向图(Undirected Graph):边没有方向,表示两个节点之间的双向关系。
  • 加权图(Weighted Graph):边具有权重,通常用于表示边的成本或距离。
  • 无权图(Unweighted Graph):所有边都没有权重。
  • 稠密图(Dense Graph):边的数量接近节点数量的平方。
  • 稀疏图(Sparse Graph):边的数量远小于节点数量的平方。

(三)图的表示方法

  • 邻接矩阵(Adjacency Matrix)
    • 使用一个二维数组表示图,其中行和列分别代表节点。
    • 对于无向图,矩阵是对称的;对于有向图,矩阵可能是不对称的。
    • 如果节点之间存在边,则对应的矩阵元素为1,否则为0(或权重值)。
    • 适用于稠密图,因为每个节点与其它节点的关系都需要存储。
  • 邻接表(Adjacency List)
    • 使用链表或数组列表来表示每个节点的所有邻居。
    • 对于每个节点,都维护一个链表或数组列表,列出所有与之相连的节点。
    • 适用于稀疏图,因为只存储实际存在的边。
  • 边集数组(Edge Set Array)
    • 存储图的所有边的一个数组或列表。
    • 通常用于简单操作,如遍历所有边。

(四)图的应用场景

  • 社交网络分析:节点可以代表用户,边可以代表朋友关系。
  • 路由算法:节点可以代表城市,边可以代表道路。
  • 电路设计:节点可以代表电子元件,边可以代表连线。
  • Web页面链接:节点可以代表网页,边可以代表超链接。

(五)图的优缺点

优点
  • 灵活性:可以灵活地表示复杂的关系。
  • 应用广泛:几乎可以应用于所有需要建模实体间关系的场景。
  • 多种搜索算法:如深度优先搜索(DFS)、广度优先搜索(BFS)等。
缺点
  • 存储消耗:尤其是使用邻接矩阵时,即使对于稀疏图也会占用大量的空间。
  • 操作复杂度:某些操作(如插入和删除边)可能比较复杂。

五、集合结构 

在数据结构中,集合结构是一种可以存储不同元素的数据结构,其中每个元素都是唯一的,没有重复的元素。

集合结构的特点包括:

  1. 元素唯一性:集合中的元素是不重复的,每个元素只能出现一次。
  2. 无序性:集合中的元素没有固定的顺序,可以随时调整元素的位置。
  3. 动态性:集合的大小可以根据需要进行调整,可以插入新的元素或者删除现有的元素。

集合结构的实现方式有多种,常见的有以下几种:

  1. 数组:使用数组实现集合结构时,需要遍历整个数组以判断元素是否重复,插入和删除操作的时间复杂度为O(n)。
  2. 链表:使用链表实现集合结构时,可以通过遍历链表来判断元素是否重复,插入和删除操作的时间复杂度为O(1)。
  3. 动态数组:动态数组是一种可以自动扩容的数组,使用动态数组实现集合结构时,可以通过调整数组的大小来动态管理集合的大小。
  4. 哈希表:哈希表是一种利用哈希函数将元素映射到存储位置的数据结构,使用哈希表实现集合结构时,可以通过哈希函数快速判断元素是否重复,插入和删除操作的平均时间复杂度为O(1)。

集合结构常用的操作包括:

  1. 插入:向集合中插入一个元素。
  2. 删除:从集合中删除一个元素。
  3. 查询:判断集合中是否包含某个元素。
  4. 遍历:遍历集合中的所有元素并进行相应的操作。

集合结构在实际应用中广泛使用,例如在数据库中用于存储唯一的用户ID或者商品编号,在网络爬虫中用于存储已经访问过的URL等。

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值