数据结构与基础算法01

提纲:
在这里插入图片描述

一.线性表

1.顺序表
顺序的存储结构,元素在内存中以顺序存储。内存中占用连续的一个区域。

顺序表的删除:
把要删除的元素后面的每一个元素向前移动一位。

顺序表的插入:
把要插入的位置后面的所有元素向后移动一位,再把要插入的元素放入该位置。

优点:方便查询
缺点:不利于插入和删除。

当执行插入操作时,如果容量不足,则需要扩容,操作为重新申请一块更大的空间,然后将原顺序表中的内容复制到新的空间。

顺序表的初始化与插入删除实现
顺序表的扩容与合并

2.链表
离散的数据结构,各个点的存储空间是离散的,通过指针联系起来。

单链表:
在这里插入图片描述

循环链表:
在这里插入图片描述

双向链表:
在这里插入图片描述

单链表:从第一个元素开始指向下一个元素,最后一个元素指向NULL。

循环链表:最后一个元素指向第一个元素

双链表:两个指针域,一个指向前一个元素,一个指向后一个元素,从两个方向连接。

链表的操作:

单链表的删除:先将前一个结点的next指向要删除结点的后一个元素,然后删除并释放掉此结点。

    Node* a1=(Node*)malloc(sizeof(Node));
    Node* a2=(Node*)malloc(sizeof(Node));
    Node* a3=(Node*)malloc(sizeof(Node));
    a1->next=a2;
    a2->next=a3;
    a3->next=NULL;
    cout<<a1->next<<endl;//未删除时

    //删除a2这个结点
    //关键步骤:将前一个元素的结点的next指向下一个节点
    a1->next=a2->next;
    //将删除的结点的内存释放
    free(a2);
    cout<<a1->next<<endl;

单链表的插入:先将新的结点指向后一个结点,再将前一个结点指向要插入的结点。

Node* a1=(Node*)malloc(sizeof(Node));
Node* a2=(Node*)malloc(sizeof(Node));
a1->next=a2;
a2->next=NULL;
//cout<<a1->next<<' '<<a2<<endl;
//插入x这个结点至a1与a2中间
Node* x=(Node*)malloc(sizeof(Node));
x->next=a1->next;//第一步
a1->next=x;
//cout<<a1->next<<' '<<x<<endl;

双链表的插入与删除操作,对比于单链表,多了一个pre指针的操作,没有本质上的区别。

链表的头指针与尾指针的作用:

头指针:头结点即在链表的首元结点(即存储实际数据的第一个节点)之前附设的一个结点,该结点的数据域可以为空,也可存放表长度等附加信息,其作用是为了对链表进行操作时,可以对空表、非空表的情况以及对首元结点进行统一处理,编程更方便。

尾指针:尾指针是指向终端结点的指针,用它来表示单循环链表可以使得查找链表的开始结点和终端结点都很方便,设一带头结点的单循环链表,其尾指针为rear,则开始结点和终端结点的位置分别是rear->next->next 和 rear, 查找时间都是O(1)。 若用头指针来表示该链表,则查找终端结点的时间为O(n)。

C++单链表的实现与操作代码

顺序表与链式表空间与时间的分析:

顺序存储的存储密度为1,而链式存储小于1。
顺序存储事先就确定好容量分配,而链式存储可以动态改变存储容量。

读取数据:顺序存储读取数据的时间复杂度为O(1),链式存储读取数据的时间复杂度为O(n)

查找数据:顺序存储O(n),链式存储O(n)

插入数据:顺序O(n),链式O(1)

删除数据:顺O(n),链O(1)

3.栈
栈可以使用顺序表或者链式表实现,即规定只能对栈顶进行操作,实现先进后出。

栈的数组实现与链式实现

4.队列

先进先出,只能从队尾插入,对头读取。

C++普通队列的顺序表实现与链式实现

双端队列可以同时在队列的两端进行插入和弹出操作。

C++双端队列的实现

阻塞队列:当队列为空时,线程读取队列阻塞,当队列为满时,线程往队列里添加元素阻塞。

C++实现阻塞队列

并发队列与阻塞并发队列待补充,没太搞懂,有了解的小伙伴欢迎给博主解惑。

二.散列表(哈希表)

1.根据关键码值(Key value)而直接进行访问,通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

2.除留余数法:将要寻址的数通过对p进行取模来当做hash表中的键值,方便直接进行查找,插入和删除等操作。。
其中:假设hash表的表长为m,那么p的取值最好为不大于m的最大质数,这样可以减少hash冲突。

3.解决hash冲突的常见方法:
开放地址法:
将发生冲突的key值,通过线性探测法hi=(h(key)+i)%m 0≤i≤m-1 ,重新计算hash值,直到hash值不再冲突,将其放入相应位置。这样做的缺点是可能会发生很长一段区间的聚集,导致后面探测的时间越来越长,增加探测时间。
于是可以采用平方探测法,这个方法就比较跳,它不会每次探测都集中在一个区间,而是会隔一段距离,可以减少时间。

开放地址法的一个缺点就是,当删除一个结点时,不能像拉链法一样直接置为空,因为这样会破坏其他同义值的寻址方式,所以只能做一个标记,表示删除。因为在散列表中,地址为空就代表查找失败的条件。

拉链法:
将整个散列表挂在单链表中,就像这样
在这里插入图片描述

这样也有一个问题,就是当挂载长度越来越大的时候,查找需要遍历链表,效率也就会越来越低
这里可以采用rehash来解决问题。

设置一个装载因子α,数值等于 所有元素个数/数组的大小,反应了hash表的装满程度。

当α大于我们自己设置的α后,我们就进行rehash。

rehash就是重新设置hash表也就是单链表的长度,一般是rehash为比原来hash表长度m的两倍大的最小的质数。然后再重新计算hash值,将其添入进单链表中。

一个非常有意思的讲hash的网站哈哈,慧能,你吃过饭了吗

三.树

1.基本概念:
结点的度:与下一层有几个结点相关联,度就是多少

树的度:整个树中度数最大的结点的度是多少,树的度就是多少

叶子结点:度为0的结点

分支结点:除了叶子结点的所有结点都是分支结点(下一层有分支)

内部结点:分支结点中除了根结点的所有结点都是内部结点(中间层的结点)

父结点:儿子的父结点

子结点:父节点的儿子结点

公式:所有结点的度之和+1 = 结点总个数
由此可以推:二叉树度为1的结点为n,度为2的结点为m,求度为0的结点的个数n0
n0 + n + m = D总
n + 2m + 1 = D总
n0 = m + 1
叶子结点的个数是度为2的结点的个数加1。

2.树,二叉树,森林的转换与遍历

树转换为二叉树:先给每个兄弟结点之间加线,然后只保留每个结点与左孩子的连线,再顺时针调整
在这里插入图片描述

森林转换为二叉树:按照树转换的方法先将每个子树转换为二叉树,第一棵树不动,将后面二叉树的根节点依次加在前一棵树根结点的右孩子上。
在这里插入图片描述

二叉树转换为树:将结点的左结点的所有右孩子与该节点连线,然后删除所有与右孩子的连线,调整。
在这里插入图片描述

二叉树转换为森林:从根结点开始,右孩子存在,则删除和右孩子的连线,再看剩下的二叉树,是否存在右孩子,存在则删除和右孩子连线,直到所有右孩子连线都删除为止,调整。
在这里插入图片描述

树的遍历:
先根遍历:先访问树的根结点,然后依次先根遍历每棵子树(类似二叉树的前序遍历)
后根遍历:依次后根遍历每棵子树,然后访问根结点(类似二叉树的后序遍历)

森林遍历:
先序遍历:先访问第一棵树,按照先根遍历方式遍历此树,直到所有树遍历完成
后序遍历:先访问第一棵树,按照后根遍历方式遍历此树,在访问根结点,直到所有树遍历完成。

二叉树遍历:
先序遍历:先访问根结点,然后遍历左子树,最后遍历右子树
中序遍历:先遍历左子树,再访问根结点,最后遍历右子树
后序遍历:先遍历左子树,再遍历右子树,最后访问根结点

森林的先序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。

3.二叉树
每个结点最多有两个子结点,分为左子结点和右子结点。

满二叉树:二叉树的每层都是满的(完整金字塔)

完全二叉树:对于n层的二叉树,其n-1层是满二叉树,第n层的结点从左到右连续排列。

所以满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树。

查找二叉树(二叉排序树)
空树或者满足以下递归条件:

1.查找树的左右子树各是一棵查找树。

2.若左子树非空,则左子树上各个结点的值均小于根节点的值

3.若右子树非空,则右子树上各个结点的值均大于根结点的值。

二叉排序树的实现和各种操作代码

二叉查找树的时间复杂度:
给定值的比较次数等于给定值节点在二叉排序树中的层数。如果二叉排序树是平衡的,则n个节点的二叉排序树的高度为Log2n+1,其查找效率为O(Log2n),近似于折半查找。如果二叉排序树完全不平衡,则其深度可达到n,查找效率为O(n),退化为顺序查找。一般的,二叉排序树的查找性能在O(Log2n)到O(n)之间。因此,为了获得较好的查找性能,就要构造一棵平衡的二叉排序树。

平衡二叉树
为了解决二叉查找树退化成一个链表的问题而诞生。

1.具有二叉查找树的全部特性

2.每个结点的左子树和右子树的高度差最多为1。

对于有n个结点的平衡二叉树,最坏的查找时间复杂度也为O(logn)

平衡二叉树的旋转:
①LL:右单旋转
在这里插入图片描述

②RR:左单旋转
在这里插入图片描述

③LR平衡旋转:先左后右
在这里插入图片描述

④RL平衡旋转:先右后左
在这里插入图片描述

平衡二叉树的插入,删除,查询,遍历等操作实现

红黑树
在二叉排序树的基础上满足以下特性:

1.结点是红色或黑色

2.根结点始终是黑色

3.叶子结点(NIL 结点)都是黑色

4.红色结点的两个直接孩子结点都是黑色(即从叶子到根的所有路径上不存在两个连续的红色结点)

5.从任一结点到每个叶子的所有简单路径都包含相同数目的黑色结点

特性5:确保没有一条路径会比其他路径长出俩倍

一棵含有n个节点的红黑树的高度至多为2log(n+1)

红黑树的变色与旋转:

当查找和修改红黑树时,不需要变色与旋转。只有当插入和删除时,才可能会改变红黑树的平衡性,需要变色和旋转。
将插入的节点着色为红色,这样不会违背特性5。

插入的三种情况:
1.当插入的结点为根结点时,直接把此结点涂为黑色。

2.当插入结点的父结点为黑色时,什么也不需要做,插入后仍然是红黑树。

3.插入结点的父结点为红色,我们分三种情况:

(1)父结点是红色,叔叔结点也是红色。
方法:将父结点设为黑色,将叔叔结点也变为黑色,将祖父结点变为红色,然后将祖父结点当做当前结点,重复操作,一直到其祖父节点是根结点无法变色的情况(根结点必须为黑色),这个时候,就开始旋转,直到满足条件。

(2)父结点是红色,叔叔结点为黑色,父结点是祖父结点的左孩子且当前结点是父结点的左孩子。
将父结点改为黑色,将祖父结点改为红色。然后以祖父结点为基准右旋。

如果父结点是祖父结点的右结点,当前结点也是父结点的右结点。
将父结点改为黑色,将祖父结点改为红色。然后以祖父结点为基准左旋。

(3)父结点是红色,叔叔结点为黑色,父结点是祖父结点的左孩子且当前结点是父结点的右孩子。
以父结点为基准,进行左旋,然后将父结点当做插入结点以上一种情况(2)来做调整。

以上内容建议大家自己画图进行分析,理解为重,不能死记硬背。

删除的情况:
鉴于博主学习红黑树的删除情况时,非常吃力,博主准备再花几天时间好好看一看删除情况,在这里博主贴两个博主认为比较好的讲解链接,咳咳,差点被一棵红黑树给搞死这两天。

红黑树删除1

红黑树删除2

为什么有了AVL树还需要红黑树?
答:虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在O(logn),但是却不是最佳的,因为平衡树要求每个结点的左子树和右子树的高度差至多等于1,这个要求实在是太严格了,导致每次进行插入和删除结点时,都会破坏平衡树的规则,进而通过左旋或者右旋来进行调整。平衡树需要频繁的进行调整,使平衡树的性能大打折扣,所以才发明红黑树。红黑树就没有高度差至多为1的要求,不会在插入和删除操作时频繁破坏红黑树的规则,所以不需要频繁调整。

B树
为什么要有B树:
B树的设计一开始是为硬盘设计的。

(1)计算机有一个局部性原理,就是说,当一个数据被用到时,其附近的数据也通常会马上被使用。

(2)所以当你用红黑树的时候,你一次只能得到一个键值的信息,而用B树,可以得到最多M-1个键值的信息。

(3)另外一方面,同样的数据,红黑树的阶数更大,B树更短,这样查找的时候当然B树更具有优势,效率也就越高。

对于B树,B树大量应用在数据库和文件系统当中,B树和B+树被用于数据库的索引。

B树是对二叉查找树的改进。它的设计思想是,将相关数据尽量集中在一起,以便一次读取多个数据,减少硬盘操作次数。B树为系统最优化大块数据的读和写操作。B树算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。

假定一个节点可以容纳100个值,那么3层的B树可以容纳100万个数据,如果换成二叉查找树,则需要20层。假定操作系统一次读取一个节点,并且根节点保留在内存中,那么B树在100万个数据中查找目标值,只需要读取两次硬盘。B 树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。

B树的结构:

  1. 根节点至少有两个子节点 ;

  2. 每个节点有M-1个key,并且以升序排列 ;

  3. 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间 ;

  4. 其它节点至少有M/2个子节点 ;

  5. 所有叶子节点都在同一层 。

在这里插入图片描述

B+树

B+树是B-树的变体,也是一种多路搜索树,其定义基本与B树相同,除了:

  1. 非叶子结点的子树指针与关键字个数相同;

  2. 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);

  3. 为所有叶子结点增加一个链指针;

  4. 所有关键字都在叶子结点出现。

在这里插入图片描述

B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。

B树的优点

B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。

B+树的优点

  1. 由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率;

  2. B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。

B树,B+树的插入,删除操作

2-3树和2-3-4树
2-3树

2-3树是一棵自平衡的多路查找树,它并不是一棵二叉树,具有如下性质:

(1)每个节点有1个或2个key,对应的子节点为2个子节点或3个子节点;

(2)所有叶子节点到根节点的长度一致;

(3)每个节点的key从左到右保持了从小到大的顺序,两个key之间的子树中所有的key一定大于它的父节点的左key,小于父节点的右key。
在这里插入图片描述

2-3树的查询时间复杂度也是为 O(logN) ,而出现这种多路查找树,主要是跟内存与磁盘交互有关。在内存IO的速度比磁盘IO要快的多的多,但是同样空间大小的内存比硬盘要贵的多的多,像TB级别的数据库不可能全部读出来放到内存中去,太过昂贵,而且也没必要,大部分数据是不经常用的,所以就需要内存与外存互相结合,而如果用平衡二叉树这种数据结构,在大数据量的情况下,树肯定会很高,此时查个数据对磁盘读个几千上万次那肯定是不行的(有人可能说把数据的索引文件全部放到内存中,然后把源数据放在硬盘中,这样在内存中定位到源数据Id,然后去外存中取源数据,这样肯定是不行的,不要以为索引文件很小,像搜索引擎的倒排索引文件比源文件还要大),所以用多路查找树这种数据结构,高阶的情况下,树不用很高就可以标识很大的数据量了,检索次数就大大减少了,用这种数据结构去磁盘中存取数据,磁盘IO次数的次数也会很少。

2-3-4树只是在2-3树的基础上进行了扩展。2-3-4树也是一棵自平衡的多路查找树,具有如下性质:

(1)任一节点只能是1个或2个或3个key,对应的子节点为2个子节点或3个子节点或4个子节点;

(2)所有叶子节点到根节点的长度一致;

(3)每个节点的key从左到右保持了从小到大的顺序,两个key之间的子树中所有的key一定大于它的父节点的左key,小于父节点的右key,对于3个key的节点,两两key之间也是如此。

在这里插入图片描述

23树和234树的插入和删除操作

堆分为小顶堆和大顶堆

小顶堆的父结点均小于子结点。

大顶堆的父结点均大于子结点。

堆和普通树的区别:

堆和普通树的区别
堆并不能取代二叉搜索树,它们之间有相似之处也有一些不同。我们来看一下两者的主要差别:

1.节点的顺序。在二叉搜索树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。

2.内存占用。普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配额为是我内存。堆仅仅使用一个数据来村塾数组,且不使用指针。

3.平衡。二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(log n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足对属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(log n) 的性能。

4.搜索。在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入、删除操作。

堆的存储,是用数组。相比较树,极大地节省了空间。

在一个存储堆的有序数组里面,有这样一个规律。

parent(i) = floor((i - 1)/2)
left(i) = 2i + 1
right(i) = 2i + 2

优先队列其实实现就是堆。不管先进先出,都是最大的或者最小的先出来。

四.图(部分)

图分为有向图和无向图。

图就是一个多对多的数据结构。在内存的存储上,图是以邻接矩阵和邻接表的方式来存储的。
(当然还有十字链表,逆邻接表等等)

无向图和有向图在邻接矩阵上的区别:

在这里插入图片描述

在这里插入图片描述

可以看到,无向图是对称的,但是有向图根据路径并不一定对称。
用邻接矩阵表示图比较好理解,但是空间复杂度太高,而且增加和删除结点的操作也比较复杂。

所以发明邻接表:

邻接表就是把图里面的每一个元素当做结点创建一个链表,然后在链表中存储与之相连的结点。

图的遍历

图的遍历分为深度优先遍历(DFS)和广度优先遍历(BFS)。

深度有序遍历类似于树的前序遍历。
广度优先遍历类似于树的层次遍历。

博主这两天差点被几棵树给整死了,本来想一口气将整个数据结构和算法写完的,但是还是得断一下,分作几篇来发,不说了,又快一点了,博主要冲冲凉继续追我的江南今何在的爱恨情仇了,希望某天冒学和数据结构都能大成。(五黑框的传奇,哈哈,懂得人击个掌)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值