数据结构-001

数据结构

1、如何避免Hash碰撞

(1)开放地址法

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

(2)再哈希法

当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不在产生为止。

(3)链地址法

将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头节点的链表的尾部。

(4)建立公共溢出区

将哈希表分为基本表和溢出表两部分,发生冲突的元素都放在溢出表中。

2、二叉查找树、红黑树、AVL树、平衡二叉树、B树、B+树

一、二叉查找树

二叉查找树也称为有序二叉查找树,满足二叉查找树的一般性质,是指一棵空树具有如下性质:

  • 任意节点左子树不为空,则左子树的值均小于根节点的值.
  • 任意节点右子树不为空,则右子树的值均大于于根节点的值.
  • 任意节点的左右子树也分别是二叉查找树.
  • 没有键值相等的节点.
2、局限性及应用

一个二叉查找树是由n个节点随机构成,所以,对于某些情况,二叉查找树会退化成一个有n个节点的线性链.如下图:

1、简介

请添加图片描述
图为一个普通的二叉查找树,大家看a图,如果我们的根节点选择是最小或者最大的数,那么二叉查找树就完全退化成了线性结构;

​因此,在二叉查找树的基础上,又出现了AVL树,红黑树,它们两个都是基于二叉查找树,只是在二叉查找树的基础上又对其做了限制。

二、AVL树

1、简介

​ AVL树是带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,左右子树树高不超过1,和红黑树相比,它是严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1).不管我们是执行插入还是删除操作。只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,**由此我们可以知道AVL树适合用于插入删除次数比较少,但查找多的情况
请添加图片描述
从上面这张图我们可以看出,任意节点的左右子树的平衡因子差值都不会大于1.

2、局限性

​ 由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树.当然,如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树.

三、红黑树

1、简介

​ 一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是red或black. 通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍.它是一种弱平衡二叉树(由于是若平衡,可以推出,相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数变少,所以对于搜索,插入,删除操作多的情况下,我们就用红黑树.

2、性质
  • 每个节点非红即黑.
  • 根节点是黑的。(根叶黑)
  • 每个叶节点(叶节点即树尾端NUL指针或NULL节点)都是黑的.
  • 如果一个节点是红的,那么它的两儿子都是黑的.(不红红)
  • 对于任意节点而言,其到叶子点树NIL指针的每条路径都包含相同数目的黑节点.(黑路同)
    这里写图片描述
    每条路径都包含相同的黑节点.
3、应用
  • 广泛用于C++的STL中,map和set都是用红黑树实现的.
  • 著名的linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块,进程的虚拟内存区域都存储在一颗红黑树上,每个虚拟地址区域都对应红黑树的一个节点,左指针指向相邻的地址虚拟存储区域,右指针指向相邻的高地址虚拟地址空间.
  • IO多路复用epoll的实现采用红黑树组织管理sockfd,以支持快速的增删改查.
  • ngnix中,用红黑树管理timer,因为红黑树是有序的,可以很快的得到距离当前最小的定时器.
  • java中TreeMap的实现.

四、B/B+树

注意B-树就是B树,-只是一个符号.

1、简介

B/B+树是为了磁盘或其它存储设备而设计的一种平衡多路查找树(相对于二叉,B树每个内节点有多个分支),与红黑树相比,在相同的的节点的情况下,一颗B/B+树的高度远远小于红黑树的高度(在下面B/B+树的性能分析中会提到).B/B+树上操作的时间通常由存取磁盘的时间和CPU计算时间这两部分构成,而CPU的速度非常快,所以B树的操作效率取决于访问磁盘的次数,关键字总数相同的情况下B树的高度越小,磁盘I/O所花的时间越少.

2、B树的性质
  • 定义任意非叶子结点最多只有M个儿子;且M>2;
  • 根结点的儿子数为[2, M];
  • 除根结点以外的非叶子结点的儿子数为[M/2, M];
  • 每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
  • 非叶子结点的关键字个数=指向儿子的指针个数-1;
  • 非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
  • 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
  • 所有叶子结点位于同一层;
    这里写图片描述
    这里只是一个简单的B树,在实际中B树节点中关键字很多的.上面的图中比如35节点,35代表一个key(索引),而小黑块代表的是这个key所指向的内容在内存中实际的存储位置.是一个指针.
3、B+树

B+树是应文件系统所需而产生的一种B树的变形树(文件的目录一级一级索引,只有最底层的叶子节点(文件)保存数据.),非叶子节点只保存索引,不保存实际的数据,数据都保存在叶子节点中.这不就是文件系统文件的查找吗?我们就举个文件查找的例子:有3个文件夹,a,b,c, a包含b,b包含c,一个文件yang.c, a,b,c就是索引(存储在非叶子节点), a,b,c只是要找到的yang.c的key,而实际的数据yang.c存储在叶子节点上.
所有的非叶子节点都可以看成索引部分

B+树的性质(下面提到的都是和B树不相同的性质)
  • 非叶子节点的子树指针与关键字个数相同;
  • 非叶子节点的子树指针p[i],指向关键字值属于[k[i],k[i+1]]的子树.(B树是开区间,也就是说B树不允许关键字重复,B+树允许重复);
  • 为所有叶子节点增加一个链指针.
  • 所有关键字都在叶子节点出现(稠密索引). (且链表中的关键字恰好是有序的);
  • 非叶子节点相当于是叶子节点的索引(稀疏索引),叶子节点相当于是存储(关键字)数据的数据层.
  • 更适合于文件系统;
    看下图:
    这里写图片描述
    非叶子节点(比如5,28,65)只是一个key(索引),实际的数据存在叶子节点上(5,8,9)才是真正的数据或指向真实数据的指针.
4、应用

B和B+树主要用在文件系统以及数据库做索引.比如MYSQL;

5、B/B+树性能分析
  • n个节点的平衡二叉树的高度为H(即logn),而n个节点的B/B+树的高度为logt((n+1)/2)+1;
  • 若要作为内存中的查找表,B树却不一定比平衡二叉树好,尤其当m较大时更是如此.因为查找操作CPU的时间在B-树上是O(mlogtn)=O(lgn(m/lgt)),而m/lgt>1;所以m较大时O(mlogtn)比平衡二叉树的操作时间大得多. 因此在内存中使用B树必须取较小的m.(通常取最小值m=3,此时B-树中每个内部结点可以有2或3个孩子,这种3阶的B-树称为2-3树)。
6、为什么说B+tree比B树更适合实际应用中操作系统的文件索引和数据索引.
  • B+-tree的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了.
  • 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当
    他们认为数据库索引采用B+树的主要原因是:
  • B树在提高了IO性能的同时并没有解决元素遍历时效率低下的问题,正是为了解决这个问题,B+树应用而生.
  • B+树只需要去遍历叶子节点就可以实现整棵树的遍历.而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低).
7、B+树相比B树优化之处
  • 1:稳定性优化——B树的关键字分布在整颗树中,同一个关键字只会出现一次。B+树可能会出现多次,所有的叶子结点中包含了全部关键字的信息,B树搜索有可能在非叶子结点就结束搜索,而B+树必须搜索到叶子结点,但是锁定磁盘块指针消耗可以忽略不计;

  • 2:相同层级数数据量增加,减少IO次数——B树中间节点会保存数据,B+树的中间节点不保存数据,磁盘页能容纳更多关键字和指针信息,更“矮胖”,树的层高能进一步被压缩;

  • 3:范围查找优化——B+树叶子结点本身依关键字的大小自小而大顺序排列而且允许链接(有序链表),只需遍历叶子节点链表即可,b树却需要重复地遍历树。

3、常见排序算法

请添加图片描述
1、冒泡排序
思路:外层循环从1到n-1,内循环从当前外层的元素的下一个位置开始,依次和外层的元素比较,出现逆序就交换,通过与相邻元素的比较和交换来把小的数交换到最前面。

a=[1,3,4,5,1,2,3]
for i in range(1,len(a)):
    for j in range(len(a)-i):
        if a[j]>a[j+1]:
            tmp=a[j]
            a[j]=a[j+1]
            a[j+1]=tmp
print(a)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wwnHmBrv-1651722870497)(/Users/hello/Desktop/面试/image/1730367-20190709233249402-580041311.gif)]

2、选择排序
思路:冒泡排序是通过相邻的比较和交换,每次找个最小值。选择排序是:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WJaCU4pz-1651722870498)(/Users/hello/Desktop/面试/image/1730367-20190709234546406-1933756989.gif)]

a=[1,3,4,5,1,2,3]
for i in range(len(a)):
    mins=i
    for j in range(i,len(a)):
        if a[j]<a[mins]:
            mins=j
    if mins!=i:
        tmp=a[i]
        a[i]=a[mins]
        a[mins]=tmp	
print(a)

3、插入排序
思路:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。可以理解为玩扑克牌时的理牌;

a=[1,3,4,5,1,2,3,9,0]
for i in range(1,len(a)):
    tmp=a[i]
    j=i
    while tmp<a[j-1] and j>0:
        a[j]=a[j-1]
        j-=1
    if i!=j:
        a[j]=tmp	
print(a)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MZaFHGFy-1651722870500)(/Users/hello/Desktop/面试/image/1730367-20190709235607059-17861797.gif)]

4、希尔排序
思路:希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。
问题:增量的序列取法?
  关于取法,没有统一标准,但最后一步必须是1;因为不同的取法涉及时间复杂度不一样,具体了解可以参考《数据结构与算法分析》;一般以length/2为算法。(再此以gap=gap*3+1为公式)

img

5、归并排序
思路:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。它使用了递归分治的思想;相当于:左半边用尽,则取右半边元素;右半边用尽,则取左半边元素;右半边的当前元素小于左半边的当前元素,则取右半边元素;右半边的当前元素大于左半边的当前元素,则取左半边的元素。
缺点:因为是Out-place sort,因此相比快排,需要很多额外的空间。

为什么归并排序比快速排序慢?

答:虽然渐近复杂度一样,但是归并排序的系数比快排大。

6、快速排序
思路:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

import numpy as np
import sys

# 设置随机种子
np.random.seed(0)
# 得到一组随机数
nums = np.random.randint(10, size=(8))
print(nums)


def QuickSort(nums, left, right):
    if left>=right:
        return 0
    bestNumber=nums[left]
    i=left
    j=right
    while i!=j:
        while j>i and nums[j]>=bestNumber:
            j-=1
        while j>i and nums[i]<=bestNumber:
            i+=1
        if i<j:
            nums[i],nums[j]=nums[j],nums[i]
    nums[j],nums[left]=nums[left],nums[j]
    QuickSort(nums,left,i-1)
    QuickSort(nums,i+1,right)


QuickSort(nums, 0, nums.size - 1)
print(nums)


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r8xyC8ux-1651722870501)(/Users/hello/Desktop/面试/image/1730367-20190710002639792-1166003167.gif)]
7、堆排序
思路:堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

在这里插入图片描述
10、基数排序
思路:基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

取得数组中的最大数,并取得位数;
arr为原始数组,从最低位开始取每个位组成radix数组;
对radix进行计数排序(利用计数排序适用于小范围数的特点);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aPtr6QPC-1651722870502)(/Users/hello/Desktop/面试/image/1730367-20190710004213327-335264969.gif)]

选择排序的不稳定例子很简单。

比如A 80 B 80 C 70 这三个卷子从小到大排序

第一步会把C和A做交换 变成C B A

第二步和第三步不需要再做交换了。所以排序完是C B A。但是稳定的排序应该是C A B

4、二叉树的前序遍历,中序遍历,后序遍历非递归写法

4.1 前序遍历【中左右】:

思路:

先不断搜寻左结点,同时将数值压入返回数组则实现了根先压入再压左子结点的情况。
直到到了左叶子结点,则从stack中取出一个结点,开始搜寻该结点的右子树部分,右子树部分重复步骤1。

vector<int> preorderTraversal(TreeNode* root) {
    
    
        vector<int> res;
        TreeNode *cur = root;
        stack<TreeNode*> stk;
        while (cur||!stk.empty()){
    
    
            while (cur){
    
    
                stk.push(cur);
                res.push_back(cur->val);
                cur=cur->left;
            }
            if (!stk.empty()){
    
    
                cur = stk.top();
                stk.pop();
            }
            if (cur) cur = cur->right;
        }
        return res;
    }


4.2 中序遍历【左中右】:

思路:

先不断搜寻左子结点,但我们不像前序遍历一样直接将数值压入,而是不断地搜寻(因为如果我们搜寻时便压入数值,则会变成【中左右】的情况)。
取出栈顶元素,压入目标值【左】(其实为该子树的根即【中】),搜寻右子树重复步骤一。

vector<int> inorderTraversal(TreeNode* root) {
    
    
        vector<int> res;
        TreeNode *cur = root;
        stack<TreeNode*> stack;
        while (cur||!stack.empty()){
    
    
            while (cur){
    
    
                stack.push(cur);
                cur = cur->left;
            }
            cur = stack.top();
            res.push_back(cur->val);
            stack.pop();
            cur = cur->right;
        }
        return res;
    }
4.3 后序遍历【左右中】:

与前序遍历思想相同,但是我们需要改变一下输出顺序
前序遍历我们得到的序列为【中左右】如果用栈压入后输出则【右左中】,离我们要的【左右中】只有前两个元素的差别,故而我们在前序遍历的序列中改变顺序【中右左】,经过栈后则变为【左右中】为我们所求。

不断搜寻右子树同时压入结点
搜索到叶子结点时,开始搜寻左子树,重复步骤1内容
将序列栈中的数值弹出,返回

vector<int> postorderTraversal(TreeNode* root) {
    
    
        TreeNode *cur = root;
        stack<TreeNode*> stk;
        stack<int> res;
        while (cur||!stk.empty()){
    
    
            while (cur){
    
    
                stk.push(cur);
                res.push(cur->val);
                cur=cur->right;
            }
            if (!stk.empty()){
    
    
                cur = stk.top();
                stk.pop();
            }
            if (cur) cur = cur->left;
        }
        vector<int> ret;
        while (!res.empty()){
    
    
            ret.push_back(res.top());
            res.pop();
        }
        return ret;
}

https://blog.csdn.net/cx__cx/article/details/116139510

猜你喜欢

转载自blog.csdn.net/weixin_43673156/article/details/124585874
今日推荐