数据结构时间复杂度问题?

数据结构基本概念及复杂度计算练习题及答案

1. 算法的计算量的大小称为计算的(B )。

2.算法的时间复杂度取决于(C )

3.从逻辑上可以把数据结构分为(C )两大类。

4.连续存储设计时,存储单元的地址(A )。

5. 以下属于逻辑结构的是(C )。

1. 数据元素是数据的最小单位 。(×)

2. 记录是数据处理的最小单位。 (×)

3. 数据的逻辑结构是指数据的各数据项之间的逻辑关系;(×)

4.程序一定是算法。(×)

5. 在顺序存储结构中,有时也存储数据结构中元素之间的关系。(×)

6. 顺序存储方式的优点是存储密度大,且插入、删除运算效率高。(×)

7. 数据结构的基本操作的设置的最重要的准则是,实现应用程序与存储结构的独立。(√)

8. 数据的逻辑结构说明数据元素之间的顺序关系,它依赖于计算机的储存结构. (×)

1.数据的物理结构包括 数据元素 的表示和数据元素间关系 的表示。

2. 对于给定的n个元素,可以构造出的逻辑结构有 集合, 线性结构 , 树形结构 , 图状结构或网状结构四种。

3.数据的逻辑结构是指数据的组织形式,即数据元素之间逻辑关系的总体。而逻辑关系是指数据元素之间的关联方式或称“邻接关系”。

4.一个数据结构在计算机中表示(又称映像) 称为存储结构。

5.抽象数据类型的定义仅取决于它的一组逻辑特性,而与在计算机内部如何表示和实现 无关,即不论其内部结构如何变化,只要它的数学特性不变,都不影响其外部使用。

6.数据结构中评价算法的两个重要指标是 算法的时间复杂度和空间复杂度 。

7. 数据结构是研讨数据的逻辑结构和物理结构,以及它们之间的相互关系,并对与这种结构定义相应的操作(运算),设计出相应的算法。

8. 一个算法具有5个特性: 有穷性 、确定性、可行性 ,有零个或多个输入、有一个或多个输出。

1. 数据结构是一门研究什么内容的学科?

答:数据结构是一门研究在非数值计算的程序设计问题中,计算机的操作对象及对象间的关系和施加于对象的操作等的学科

2. 数据元素之间的关系在计算机中有几种表示方法?各有什么特点?

    (1)顺序存储方式。数据元素顺序存放,每个存储结点只含一个元素。存储位置反映数据元素间的逻辑关系。存储密度大,但有些操作(如插入、删除)效率较差。

    (2)链式存储方式。每个存储结点除包含数据元素信息外还包含一组(至少一个)指针。指针反映数据元素间的逻辑关系。这种方式不要求存储空间连续,便于动态操作(如插入、删除等),但存储空间开销大(用于指针),另外不能折半查找等。

    (3)索引存储方式。除数据元素存储在一地址连续的内存空间外,尚需建立一个索引表,索引表中索引指示存储结点的存储位置(下标)或存储区间端点(下标),兼有静态和动态特性。

    (4)散列存储方式。通过散列函数和解决冲突的方法,将关键字散列在连续的有限的地址空间内,并将散列函数的值解释成关键字所在元素的存储地址,这种存储方式称为散列存储。其特点是存取速度快,只能按关键字随机存取,不能顺序存取,也不能折半存取。

3. 数据类型和抽象数据类型是如何定义的。二者有何相同和不同之处,抽象数据类型的主要特点是什么?使用抽象数据类型的主要好处是什么?

答:数据类型是程序设计语言中的一个概念,它是一个值的集合和操作的集合。如C语言中的整型、实型、字符型等。整型值的范围(对具体机器都应有整数范围),其操作有加、减、乘、除、求余等。实际上数据类型是厂家提供给用户的已实现了的数据结构。“抽象数据类型(ADT)”指一个数学模型及定义在该模型上的一组操作。“抽象”的意义在于数据类型的数学抽象特性。抽象数据类型的定义仅取决于它的逻辑特性,而与其在计算机内部如何表示和实现无关。无论其内部结构如何变化,只要它的数学特性不变就不影响它的外部使用。抽象数据类型和数据类型实质上是一个概念。此外,抽象数据类型的范围更广,它已不再局限于机器已定义和实现的数据类型,还包括用户在设计软件系统时自行定义的数据类型。使用抽象数据类型定义的软件模块含定义、表示和实现三部分,封装在一起,对用户透明(提供接口),而不必了解实现细节。抽象数据类型的出现使程序设计不再是“艺术”,而是向“科学”迈进了一步。

(1)在数据结构课程中,数据的逻辑结构,数据的存储结构及数据的运算之间存在着怎样的关系?

答:数据的逻辑结构反映数据元素之间的逻辑关系(即数据元素之间的关联方式或“邻接关系”),数据的存储结构是数据结构在计算机中的表示,包括数据元素的表示及其关系的表示。数据的运算是对数据定义的一组操作,运算是定义在逻辑结构上的,和存储结构无关,而运算的实现则是依赖于存储结构。

(2)若逻辑结构相同但存储结构不同,则为不同的数据结构。这样的说法对吗?举例说明之。

答: 逻辑结构相同但存储不同,可以是不同的数据结构。例如,线性表的逻辑结构属于线性结构,采用顺序存储结构为顺序表,而采用链式存储结构称为线性链表。

(3)在给定的逻辑结构及其存储表示上可以定义不同的运算集合,从而得到不同的数据结构。这样说法对吗?举例说明之。

答:栈和队列的逻辑结构相同,其存储表示也可相同(顺序存储和链式存储),但由于其运算集合不同而成为不同的数据结构。

(4)评价各种不同数据结构的标准是什么?

答:数据结构的评价非常复杂,可以考虑两个方面,一是所选数据结构是否准确、完整的刻划了问题的基本特征;二是是否容易实现(如对数据分解是否恰当;逻辑结构的选择是否适合于运算的功能,是否有利于运算的实现;基本运算的选择是否恰当。)

5.评价一个好的算法,您是从哪几方面来考虑的?

答:评价好的算法有四个方面。一是算法的正确性;二是算法的易读性;三是算法的健壮性;四是算法的时空效率(运行)。

6.解释和比较以下各组概念

(1)算法的时间复杂性

答:算法的时间复杂性是算法输入规模的函数。算法的输入规模或问题的规模是作为该算法输入的数据所含数据元素的数目,或与此数目有关的其它参数。有时考虑算法在最坏情况下的时间复杂度或平均时间复杂度。

答: 算法是对特定问题求解步骤的描述,是指令的有限序列,其中每一条指令表示一个或多个操作。算法具有五个重要特性:有穷性、确定性、可行性、输入和输出。 

答:频度。在分析算法时间复杂度时,有时需要估算基本操作的原操作,它是执行次数最多的一个操作,该操作重复执行的次数称为频度。 

7. 根据数据元素之间的逻辑关系,一般有哪几类基本的数据结构?

答:集合、线性结构、树形结构、图形或网状结构。

8.对于一个数据结构,一般包括哪三个方面的讨论?

答:逻辑结构、存储结构、操作(运算)。

9.分析下面语句段执行的时间复杂度。

分析以下程序段的时间复杂度。 
 
  • (2)是n^2,这个程序不应该这么写的,这种递归效率很低的,当然太小了感觉不到啦,,。可以这么写

本文我们来介绍一下编程中常见的一些数据结构。

为什么要学习数据结构?

随着业务场景越来越复杂,系统并发量越来也高,要处理的数据越来越多,特别是大型互联网的高并发、高性能、高可用系统,对技术要求越来越高,我们引入各种中间件,这些中间件底层涉及到的各种数据结构和算法,是其核心技术之一。如:

  • ElasticSearch中用于压缩倒排索引内存存储空间的FST,用于查询条件合并的SkipList,用于提高范围查找效率的BKDTree;
  • 各种分库分表技术的核心:hash算法;
  • Dubbo或者Nginx等的负载均衡算法;
  • MySQL索引中的B树、B+树等;
  • Redis使用跳跃表作为有序集合键的底层实现之一;
  • J.U.C并发包的各种实现的阻塞队列,AQS底层实现涉及到的链式等待队列;

可以发现,数据结构和算法真的是无处不在,作为一个热爱技术,拒绝粘贴复制的互联网工程师,怎么能不掌握这些核心技术呢?

与此同时,如果你有耐心听8个小时通俗易懂的数据结构入门课,我强烈建议你看一下以下这个视频,来自Google一位工程师:


阅读完本文,你将了解到一些常见的数据结构(或者温习,因为大部分朋友大学里面其实都是学过的)。在每个数据结构最后一小节都会列出代码实现,以及相关热门的算法题,该部分需要大家自己去探索与书写。只有自己能熟练的编写各种数据结构的代码才是真正的掌握了,大家别光看,动手写起来。阅读完本文,您将了解到:

  • 抽象数据类型与数据结构的关系;
  • 如何评估算法的复杂度;
  • 了解以下数据结构,并且掌握其实现思路:数组,链表,栈,队列,优先级队列,索引式优先队列,二叉树,二叉搜索树BST,平衡二叉搜搜书BBST,AVL树,HashTable,并查集,树状数组,后缀数组。
  • 文章里面不会贴这些数据结构的完整实现,但是会附带实现的链接,同时每种数据类型最后一节的相关实现以及练习题,建议大家多动手尝试编写这些练习题,以及尝试自己动手实现这些数据结构。

抽象数据类型(ADT abstract data type):是数据结构的抽象,它仅提供数据结构必须遵循的接口。接口并未提供有关应如何实现某种内容或以哪种编程语言的任何特定详细信息。

下标列举了抽象数据类型和数据结构之间的构成关系:

我们一般会关注程序的两个问题:

  • 时间复杂度:这段程序需要花费多少时间才可以执行完成?
  • 空间复杂度:执行这段代码需要消耗多大的内存?

有时候时间复杂度和空间复杂度二者不能兼得,我们只能从中取一个平衡点。

下面我们通过Big O表示法来描述算法的复杂度。

Big-O表示法给出了算法计算复杂性的上限。

T(n) = O(f(n)),该公式又称为算法的渐进时间复杂度,其中f(n)函数表示每行代码执行次数之和,O表示执行时间与之形成正比例关系。

常见的时间复杂度量级,从上到下时间复杂度越来越大,执行效率越来越低:

下面是我从 Big O Cheat Sheet引用过来的一张表示各种度量级的时间复杂度图表:

所谓Big-O表示法,就是要得出对程序影响最大的那个因素,用于衡量复杂度,举例说明:

练习:请看看下面代码的时间复杂度:

第三个如何得出对数?假设循环x次之后退出循环,也就是说 2^x = n,那么 x = log2(n),得出O(log(n))

空间复杂度是对一个算法在运行过程中占用存储空间的大小的衡量。

  • O(1):存储空间不随变量n的大小而变化;

2.3、常用数据结构复杂度

一些常用的数据结构的复杂度(注:以下表格图片来源于 Big O Cheat Sheet):

2.4、常用排序算法复杂度

O:表示渐近上限,即最差时间复杂度;

Θ:表示渐近紧边界,即平均时间复杂度;

Ω:表示渐近下界,即最好时间复杂度;

静态数组是固定长度的容器,其中包含n个可从[0,n-1]范围索引的元素。

问:“可索引”是什么意思?

答:这意味着数组中的每个插槽/索引都可以用数字引用。

  • 1)存储和访问顺序数据
  • 3)由IO例程用作缓冲区
  • 4)查找表和反向查找表
  • 5)可用于从函数返回多个值
  • 6)用于动态编程中以缓存子问题的答案

3.1.2、静态数组特点

  • 只能由数组下标访问数组元素,没有其他的方式了;
  • 下标超过范围了会触发数组越界错误。

动态数组的大小可以增加和缩小。

3.2.1、如何实现一个动态数组

  • 创建具有初始容量的静态数组;
  • 将元素添加到基础静态数组,同时跟踪元素的数量;
  • 如果添加元素将超出容量,则创建一个具有两倍容量的新静态数组,然后将原始元素复制到其中。

  • 在许多列表,队列和堆栈实现中使用;
  • 非常适合创建循环列表;
  • 可以轻松地对诸如火车等现实世界的物体进行建模;
  • 某些特定的Hashtable实现用于处理散列冲突;
  • 用于图的邻接表的实现中。

Head:链表中的第一个节点;

Tail:链表中的最后一个节点;

Pointer:指向下一个节点;

Node:一个对象,包含数据和Pointer。

这里使用双向链表作为例子进行说明。

从链表头遍历,直到第三个节点,然后执行如下插入操作:

遍历到节点位置,把新节点指向前后继节点:

后继节点回溯连接到新节点,并移除旧的回溯关系:

前继节点遍历连接到新节点,并移除旧的遍历关系:

注意指针处理顺序,避免在添加过程中导致遍历出现异常。

从链表头遍历,直到找到c节点,然后把c节点的前继节点连接到c的后继接节点:

把c节点的后继节点连接到c的前继节点:

移除多余指向关系以及c节点:

同样的,注意指针处理顺序,避免在添加过程中导致遍历出现异常。

堆栈是一种单端线性数据结构,它通过执行两个主要操作(即推入push和弹出pop)来对现实世界的堆栈进行建模。

  • 文本编辑器中的撤消机制;
  • 用于编译器语法检查中是否匹配括号和花括号;
  • 建模一堆书或一叠盘子;
  • 在后台使用,通过跟踪以前的函数调用来支持递归;
  • 可用于在图上进行深度优先搜索(DFS)。

给定一个由以下括号组成的字符串:()[] {},确定括号是否正确匹配。

凡是遇到( { [ 都进行push入栈操作,遇到 ) } ] 则pop栈中的元素,看看是否与当前处理的元素匹配:

匹配完成之后,栈必须是空的。

队列是一种线性数据结构,它通过执行两个主要操作(即入队enqueue和出队dequeue)来对现实世界中的队列进行建模。

队列底层可以使用数组或者链表实现

  • 任何排队等候的队伍都可以建模为Queue,例如在电影院里的队伍;
  • 可用于有效地跟踪最近添加的x个元素;
  • 用于Web服务器请求管理,保证服务器先接受的先处理;
  • 图的广度优先搜索(BFS)。

6.2.1、请用队列实现图的广度优先遍历

  • 基于数组实现的Queue:
  • 基于链表实现的Queue:

优先级队列是一种抽象数据类型(ADT),其操作类似于普通队列,不同之处在于每个元素都具有特定的优先级。 优先级队列中元素的优先级决定了从PQ中删除元素的顺序。

注意:优先级队列仅支持可比较的数据,这意味着插入优先级队列的数据必须能够以某种方式(从最小到最大或从最大到最小)进行排序。 这样我们就可以为每个元素分配相对优先级。

为了实现优先级队列,我们必须使用到堆 Heap。

堆是基于树的非线性结构DS,它满足堆不变式:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;

二叉堆是一种特殊的堆,其满足:

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

在同级兄弟或表亲之间没有隐含的顺序,对于也没有隐含的顺序。

堆通常使用隐式堆数据结构实现,隐式堆数据结构是由数组(固定大小或)组成的,其中每个元素代表一个树节点,其父/子关系由其索引隐式定义。将元素插入堆中或从堆中删除后,可能会违反堆属性,并且必须通过交换数组中的元素来平衡堆。

  • 在Dijkstra最短路径算法的某些实现中使用;
  • 每当您需要动态获取“最佳”或“次佳”元素时;
  • 用于霍夫曼编码(通常用于无损数据压缩);
  • 最佳优先搜索(BFS)算法(例如A*)使用PQ连续获取下一个最有希望的节点;
  • 由最小生成树(MST)算法使用。

7.3、最小堆转最大堆

问题:大多数编程语言的标准库通常只提供一个最小堆,但有时我们需要一个最大PQ。

  • 由于优先级队列中的元素是可比较的,因此它们实现了某种可比较的接口,我们可以简单地取反以实现最大堆;
  • 也可以先把所有数字取反,然后排序插入PQ,然后在取出数字的时候再次取反即可;

优先级队列通常使用堆来实现,因为这使它们具有最佳的时间复杂性。

优先级队列是抽象数据类型,因此,堆并不是实现PQ的唯一方法。 例如,我们可以使用未排序的列表,但这不会给我们带来最佳的时间复杂度,以下数据结构都可以实现优先级队列:

这里我们选取二叉堆来实现,二叉堆是一颗完全二叉树。

7.4.1、二叉堆排序映射到数组中

二叉堆索引与数组一一对应:

二叉堆排好序之后,即按照索引填充到数组中:

  • i节点左叶子节点:2i + 1
  • i节点右叶子节点:2i + 2

7.4.2、添加元素到二叉堆

如下图,首先追加到最后一个节点,然后一层一层的跟父节点比较,如果比父节点小,则与父节点交换位置。

7.4.3、从二叉堆移除元素

  • 第一个元素与最后一个元素交换位置,然后删除掉最后一个元素;

  • 第一个元素尝试sink 下沉操作:一直与子节点对比,如果大于任何一个子节点,则与该子节点对换位置;

  • 依次遍历数组,找到值为7的元素,让该元素与最后一个元素对换,然后删除掉最后一个元素;

  • 被删除索引对应节点尝试进行sink 下沉操作:与所有子节点比较,判断是否大于子节点,如果小于,那么就与对应的子节点交换位置,然后一层一层往下依次对比交换;

  • 如果最终该元素并没有实际下沉,那么尝试进行swim 上浮操作:与父节点比较,判断是否小于父节点,如果是则与父节点对换位置,然后一层一层往上依次对比交换;

思考:请问如何构造一个小顶堆呢?

遍历数组,所有元素依次与子节点对比,如果大于子节点则交换。

7.5、尝试让删除操作时间复杂度变为O(log(n))

以上删除算法的效率低下是由于我们必须执行线性搜索**O(n)**以找出元素的索引位置。

**我们可以尝试使用哈希表进行查找以找出节点的索引位置。**如果同一个值在多个位置出现,那么我们可以维护一个特定节点值映射到的索引的Set或者Tree Set中。

这样我们在删除的时候就可以通过HashTable定位到具体的元素了。

索引优先级队列(Indexed Priority Queue IPQ)是传统的优先级队列变体,除了常规的PQ操作之外,它还提供了索引用于支持键值对的快速更新和删除。

我们知道前面的优先级队列的元素都是存放到一个list里面的,我们想知道知道某一个值在优先级队列中的位置,也是需要遍历一个个对比才知道的,要是有重复的值,那就区分不了了。既然找不到元素,那么对元素的更新和删除也就无从说起了。

为此,我们引入了如下两个索引:节点索引ki位置索引im

  • 请查找节点ki所在的优先级位置:可以很快可以从表1中找到 pm[ki];
  • 请查找优先级位置im存的是什么节点:可以很快从表2中找到节点的索引 ki[im]

与构造或更新删除PQ不同的是,IPQ需要额外维护这些索引的关系。

**二叉树(Binary Tree)**是每个节点最多具有两个子节点的树;

**二叉搜索树(Binary Search Tree)**是满足以下条件二叉树:左子树的元素较小,右子树的元素较大。

  • 二叉搜索树(BST)元素必须具有可比性,以便我们可以在树中对其进行排序;
  • 插入元素时,我们将其值与当前节点中存储的值进行比较:
    • 小于节点值:向下递归左子树;
    • 大于节点值:向下递归右子树;
    • 等于节点值:处理重复值;
    • 不存在节点:创建新节点。

这种情况就变为了线性结构,比较糟糕,这就是平衡二叉搜索树出现的原因。

移除元素可以分为两步:

  • 找到我们想要移除的元素;
  • 如果存在后续节点,那么我们用后续节点替换掉要删除的节点;

移除会有以下三种情况:

9.2.2.1、移除的元素是一个叶子节点

找到对应待移除的节点,直接删除掉即可:

9.2.2.2、移除的元素下面有左子树或者右子树

如果移除的元素下面带有左子树或者右子树,那么:找到对应待移除的节点,用子树的根节点作为移除元素的后继者,替换掉需要移除的元素即可:

9.2.2.3、移除的元素下面有左子树和右子树

如果移除的元素下面带有左子树和右子树,那么应该用左子树还是右子树中的节点作为删除元素的后继者呢?

答案是两者都可以! 后继者可以是左侧子树中的最大值,也可以是右侧子树中的最小值。

下面我们执行remove(8),统一选择使用右子树中的最小值。

  1. 查找到需要删除的元素;

  2. 在其右子树中寻找到最小值的节点;

  3. 最小值的节点和待删除元素的值互换;

  4. 使用9.2.2.2的步骤删除掉原来最小值节点位置的节点;

BST搜索元素会出现以下四种情况之一:

  • 我们命中了一个空节点,这时我们知道该值在我们的BST中不存在;
  • 比较器值等于0,说明找到了元素;
  • 比较器值小于0,说明元素在左子树中;
  • 比较器值大于0,说明元素在右子树中。

可以分为深度优先遍历和广度优先遍历。而深度优先遍历又分为:前序遍历、中序遍历、后序遍历、层序遍历。

9.3.1、深度优先遍历

深度优先遍历都是通过递归来实现的,对应的数据结构为栈。

在递归方法最开始处理节点信息,代码逻辑:

在递归调用完左子节点,准备右子节点之前处理节点信息,代码逻辑:

二叉搜索树使用中序遍历,会得到一个排好序的列表。

在递归完左右子树之后,再处理节点信息,代码逻辑:

9.3.2、广度优先遍历

在广度遍历中,我们希望一层一层的处理节点信息,常用的数据结构为队列。每处理一个节点,同时把左右子节点放入队列,然后从队列中取节点进行处理,处理的同时把左右子节点放入队列,反复如此,直至处理完毕。

平衡二叉搜索树(Balanced Binary Search Tree BBST)是一种自平衡的二叉搜索树。所以自平衡意味着会自行调整,以保持较低(对数)的高度,从而允许更快的操作,例如插入和删除。

大多数BBST算法核心组成部分是:树不变式树旋转的巧妙用法。

树不变式:是在每次操作后必须满足的属性/规则。为了确保始终满足不变式,通常会应用一系列树旋转

在树旋转的过程中需要确保保持BST的不变式:左子树比较小,右子树比较大。

为此,我们可以使用以下操作实现右旋转,注意观察宣传前后,BST的不变式:

为什么可以这样变换呢?

还是那个原则:BST不变式

我们在变换操作的时候只要确保这个条件成立即可,即保持BST不变性成立的前提下,我们可以对树中的值和节点进行随机变换/旋转。

注意,如上图,如果a节点还有父节点p,那么就把p节点原来指向a节点变更为指向b节点。

在某些需要经常方位父节点或者同级节点的BBST中,我们就不是像上面那样最多更新3个指针,而是必须更新6个指针了,操作会复制些许。

BBST通过在不满足其不变性时执行一系列左/右树旋转来保持平衡。

AVL树是平衡二叉搜索树的一种,它允许以O(log(n))的复杂度进行插入、搜索和删除操作。

其中H(x)是节点的高度,为x和最远的叶子之间的边数

AVL树中使其保持平衡的不变形是要求平衡因子BF始终为-1、0或者1。

10.2.1、节点存储信息

  • 节点存储的实际值,此值必须可以比较;

  • 指向左右子节点的指针。

当节点的BF不为-1、0或者1的时候,使用树旋转来进行调整。可以分为几种情况:

参考BST小节的删除逻辑,与之不同的是,在删除元素之后,需要执行多一个自平衡的过程。

我要回帖

更多关于 计算时间复杂度的例题 的文章

 

随机推荐