精华内容
下载资源
问答
  • 卷积神经网络用于振动光谱数据分析

    千次阅读 热门讨论 2019-07-21 14:59:28
    卷积神经网络在振动光谱数据上的应用卷积神经网络用于振动光谱数据分析Convolutional neural networks for vibrational spectroscopic data analysis(一)摘要和简介(二)方法2.1 寻找重要的光谱区域2.2 卷积核中...


    卷积神经网络用于振动光谱数据分析

    Convolutional neural networks for vibrational spectroscopic data analysis

    Convolutional neural networks for vibrational spectroscopic data analysis

    Jacopo Acquarelli, Twan van Laarhoven, Jan Gerretzen, Thanh N. Tran, Lutgarde M.C. Buydens, Elena Marchiori (2017)


    (一)摘要和简介

    这篇文章的研究结果表明,卷积神经网络可以有效地对振动光谱数据进行分类,识别重要的光谱区域。CNN是目前最先进的图像分类和语音识别技术,可以学习数据的可解释性表示。CNN的这些特性减少了对预处理和突出重要光谱区域的需求,而这两者都是分析振动光谱数据的关键步骤。

    振动光谱数据的化学计量分析通常依赖于基线校正、散射校正和噪声去除等预处理方法,这些方法在模型建立之前应用于光谱。预处理是一个关键的步骤,因为即使在简单的问题中使用“合理的”预处理方法也可能降低最终模型的性能。

    作者开发了一种新的基于CNN的方法,并提供了一个配套的公开可用的软件。它基于一个简单的CNN架构,只有一个卷积层(所谓浅CNN)他们的方法优于化学计量学标准分类分类算法(如PLS)应用于未进行过预处理的测试数据时的准确性(平均准确率86%,而PLS为62%)它甚至在预处理过的测试数据上达到了更好的性能(平均准确率96%,而PLS为89%)为了便于解释,作者的方法还包括了一个寻找重要光谱区域的过程,从而有助于结果的定性解释。

    振动光谱学涉及到红外(IR)和拉曼光谱的特殊光学技术。振动光谱数据分类模型将输入对象(光谱)映射到所需的输出(类分配)。在此背景下设计一个分类模型是一项具有挑战性的任务,而这一任务在不同的领域都有所应用,如制药、聚合物、法医学、环境、食品科学以及医学。目前流行的化学计量学技术包括人工神经网络、支持向量机和线性判别分类器。这些技术通常应用于预处理过的数据。在这方面的主要困难之一是就使用何种光谱预处理方法和最佳设置达成共识。

    数据预处理用于提高后续多元分析的稳健性和准确性,并通过纠正与光谱数据采集相关的问题来提高数据的可解释性。预处理方法通常取决于研究的目的和所使用的技术(拉曼或红外)已经开发了大量用于振动光谱数据的预处理方法和软件。这些方法通常是基于一些任意的标准来选择的,例如“它在以前的数据集上运行良好”。最近已经表明,即使对于相对简单的问题,大多数“合理的”预处理方法及其各自的参数设置实际上可能会降低最终模型的性能。一般来说,相同的预处理技术对于一个数据集可能很有效,但是当应用于使用不同的机器、设置或样本矩阵生成的另一个数据集时,则不起作用。

    虽然化学计量学研究团体迄今为止主要集中在选择一个好的预处理方法的问题上,但是机器学习团体考虑了一些相关的问题,即学习数据的表示,识别和解开隐藏在数据中的潜在解释因素。尤其是卷积神经网络(CNNs)受到动物视觉皮层生物过程的启发(在动物视觉皮层中,细胞对视觉场的小的子区域很敏感)

    CNN是多层感知器(MLP)的变体,MLP是一种前馈人工神经网络(ANN)模型(如下图)它将输入数据映射到一组适当的输出。用于分类的ANN在过去已被应用于振动光谱数据。然而,这些计算模型有两个主要问题。首先,它们容易过拟合,导致新数据的性能很差。其次,无法对分类器进行解释(训练后的神经网络被视为一个“黑匣子”)
    在这里插入图片描述
    机器学习的最新进展使得CNN可以被用来解决这两个问题。CNN的设计考虑了输入数据的空间信息:它们通过加强相邻层神经元之间的局部连接模式,探索空间-局部相关性。与传统的神经网络相比,CNN的参数更少,并且通过嵌入正则化技术,对过拟合问题具有更强的鲁棒性。这些网络的每个卷积层的输出与输入频谱的小区域直接相关。因此,训练后CNN可以用来从分类器中识别输入数据的重要区域。

    CNN是目前二维数据(图像)分类的前沿技术,近年来也被应用于高光谱遥感场景的三维数据分类。用于图像分类的CNN使用池化层和全连接层。该文章主要关注一维数据(振动光谱数据),不使用池化层,而是利用卷积核和步长对数据进行平滑处理。步长使最终的模型更容易用于识别重要的光谱区域,而不是池化。此外,作者根据目标(平滑输入信号,在该例中是一个振动频谱)使用了一个新的正则项。这样,CNN能够更容易地适应不同的频谱输入,从而推广到其他数据。这是CNN在振动光谱数据重要区域的分类和识别上的第一次应用。

    作者证明了一种基于浅层CNN的简单方法(如下图)比偏最小二乘回归线性判别分析(PLS-LDA)和kNN的分类精度有明显的提高,分别考虑了两种场景:即不进行任何预处理,以及对输入光谱采用了一种最优的预处理方法。此外,作者的研究结果还表明CNN比PLS-LDA更少地依赖于预处理(该软件提供了公开的获取途径
    在这里插入图片描述
    结果的解释在化学计量学中与分类一样重要,这也是为什么PLS-LDA是化学计量学中使用的一种标准方法的原因。在作者使用的方法中,他们通过对卷积层的输出应用特征选择来检测重要的光谱区域:卷积提供的光谱的新表示突出了被认为相关的光谱区域。

    总的来说,这篇文章的主要创新点为:

    • 使用非标准CNN:浅层架构(只有一个隐藏的卷积层)没有池化层
    • 为CNN设计了一个自定义的损失函数,其中包含一个新的正则项,以加强附近特征之间的相似性
    • 通过寻找重要的光谱区域来增强CNN

    实验结果表明,该方法为振动光谱数据的分类和解释提供了有力的工具。

    (二)方法

    本节将首先简要介绍ANN的一般原理。接下来描述该文章为了使神经网络适合于振动光谱数据的数据分析所做出的修改。最后详细描述CNN方法在光谱特征选择上的应用。

    ANN由一组相互连接的神经元组成。神经元是神经网络的基本单位,由所谓的“激活函数”(将神经元的输入转换为输出)进行区分。神经元按层组织,每层的神经元都具有相同的激活函数。MLP是一个前馈神经网络,同一层单元之间没有连接(前馈意味着也没有从神经元到前一层的连接)

    可以将层分为3组:输入层、隐藏层和输出层。输入层为第一层,一般具有线性激活函数。输出层是最后一层,一般具有线性或softmax激活函数,分别用于回归和分类。MLP一般有一个或多个具有相同非线性激活函数的隐层,每一层的每一个单元都使用加权连接连接到下一层的每一个单元(全连接)这些权重通常都会随机初始化并在训练阶段以有监督的方式进行学习。为此,利用反向传播和梯度下降法的不同变体,根据网络预测误差的目标函数,求出局部最小值。

    使用全连接层意味着需要训练相当数量的权重,而这个数量取决于每一层的单元数。当只有几个样本可用来训练权重时,网络很容易过拟合。这是为什么ANN在化学计量学数据分析中不常用的主要原因之一,尽管它们非常适合处理高度非线性的问题(例如,可能由于数据预处理不当而发生的问题)。

    作者试图通过使用MLP的变体来克服这个限制,使用非完全连接的层(即需要训练的权重更少)并在目标函数中引入正则项。正则化通过赋予小的权重,增强了神经网络在训练数据之外的适用性。这些方法通常更可取,因为从某种意义上说,较小的权重意味着较低的复杂性,因此可以更容易地解释数据。

    作者使用的是卷积层,而不是全连接层(因此得名CNN)首先对输入使用一个卷积核做卷积操作。一般来说,一个或多个卷积核可用于捕获每个卷积层输入数据的不同的属性,而付出的代价是权重的增加。

    卷积操作是通过从第一个到最后一个输入元素以某一个固定的步长 stride 移动一个包含NN个元素的卷积核 k=iNwi\mathbf{k}=\sum_{i}^{N}w_i。由输入生成的一个新的表示是利用整个频谱上的邻近特征得到的,为此每个内核都会被反复应用于输入本身。每次应用后,卷积的结果会作为输入提供给一个经过校正的线性函数,ϕ(x)=max(0,x)\phi(x)=max(0,x),通常用作卷积层的激活函数。因此,每个核通过修正的线性函数,产生了输入的不同表示形式,并独立地连接到下一层。
    在这里插入图片描述
    与全连接层不同,在卷积层上,唯一需要学习的权值是卷积核的权值,这种权重数量的减少有利于网络泛化性能的提升。输出层使用softmax激活函数,这是分类任务的常见选择,因为它可以将预测转化为非负值,并将其标准化,从而得到类的概率分布:softmaxk(x)=eWkTxj=1neWjTxsoftmax_k(x)=\frac{e^{W_k^Tx}}{\sum_{j=1}^ne^{W_j^Tx}}其中,xx是输入向量,nn是输出层的节点个数(或者说类别个数)WkW_k是第k个节点的权重。

    作为目标,作者考虑了由交叉熵误差损失组成的下列函数,并加入了正则项:
    在这里插入图片描述
    其中,yn^ψ(wxn)\hat{y_n} \equiv \psi(\mathbf{w}\cdot\mathbf{x}_n)是网络的输出,ψ()\psi(\cdot)是激活函数,xnx_n是第n个样本,w\mathbf{w}是权重,yny_n是目标标签,Shift()˙Shift(\dot)是将数组的元素向左移动一个位置的操作。除了标准L2范数外,作者还使用了“近似L2范数”,这有助于网络保持相邻的输入变量(即振动光谱数据的波数)之间的相关性,以惩罚相邻权重之间的巨大差异。对于振动光谱数据,不期望这些变化,因为频谱在某个波数上的值依赖于相邻的波数值。

    作者使用的是一维核,因为每个样本(即频谱)都表示为一维数组(向量)他们还使用了一个全链接的输出层(在卷积层之后)它的单元数等于类的数量。在这个输出层上使用softmax激活函数可以获得网络对输入样本的类预测。在神经网络的训练过程中,使用随机梯度下降(SGD)更新规则这一标准技术。

    对于卷积核和输出层的权重的初始化,作者选择的是“Glorot”初始化,因为可以通过跟踪用于随机化的种子来复制初始化。因此,模型的参数及其值的范围是:

    • 卷积层的卷积核个数:#kernels1,2,4\#kernels\in{ 1,2,4}
    • 卷积核的大小:N[2,91]N\in[2,91]
    • 卷积步长:S[1,39]S\in[1,39]
    • 正则项中的参数::λ1,λ2=10n\lambda_1,\lambda_2=10^n,其中n[3,3]n\in[-3,3]
    • SGD更新规则中的 momentum:momentum0.1[2,9]momentum\in0.1*[2,9]
    • 学习率:lr=10nlr=10^n,其中n[8,1]n\in[-8,-1]

    为了找到参数值的最佳组合,作者在训练阶段使用随机网格搜索交叉验证框架(RGS-CV)来选择精度最高的配置。然后利用所有训练数据对该模型进行了改造,并将其应用于试验数据中,获得了较好的分类精度。

    他们将得到的方法称为CNNVS,即用于振动光谱数据分类的卷积神经网络。

    2.1 寻找重要的光谱区域

    建立高质量的分类模型和识别潜在的重要光谱区域是振动光谱数据分析的两个重要方面。偏最小二乘回归-线性判别分析(PLS-LDA)等方法可以通过在潜在变量空间中表示输入特征来寻找重要的输入特征。虽然PLS-LDA可能提供不可靠的适应症,尤其是对于大量不相关数据的数据变体,但是它可以反馈变量重要性,这使得它在化学计量学数据分析中的受欢迎程度超过了更强大的方法,如具有非线性内核的支持向量机(如SVM,其特征相关性无法从模型中轻易量化)

    将特征选择过程集成到CNNVS中来识别重要的光谱区域是很简单的。训练后的CNN卷积层的每个输出节点对应一个应用于频谱特定区域的核。因此,可以将特征选择算法应用到这种输出中去寻找重要的特征;然后将这些特征返回到光谱的相应区域。

    在作者的分析中,使用了稳定性特征选择。稳定性特征选择是将特征选择应用于随机选取的数据子集(即训练后的CNNVS卷积层的输出)该特征选择是通过重新训练CNNVS的最后一层来执行的,该层可以被视为逻辑回归网络,该逻辑回归网络具有响应于光谱的不同子集的卷积层的输出作为输入,以及类别预测作为输出。每次再训练后,选择系数为正的特征。然后,通过考虑每个特征被选择的次数,合并子集上的结果,从而为每个特征生成一个分数。

    稳定性特征选择的动力来自于该方法在特征数远大于样本数的问题中的有效性,这是大多数振动光谱数据集的情况。我们称之为识别重要光谱区域的结果方法CNNVSfs,卷积神经网络用于包含特征选择的振动光谱数据分类。

    2.2 卷积核中元素的解释

    如第2节所述,可以在一个卷积层上使用多个内核。目的是利用不同的核来捕捉光谱的不同性质。它们数量上的限制主要与过拟合有关,因此作者使用模型选择方法RGS-CV(在第2节末尾描述过)来排除那些对于所考虑的数据集而言具有太多卷积核的模型。

    对于解释光谱上的伪影,以及推测每个核的行为与已知的预处理方法之间的关系,需要对学习到的核进行分析。

    (三)评估研究

    为了比较评估方法的性能,作者收集了一些公开可用的振动光谱数据集进行分类。他们将CNNVS与PLS-LDA进行了比较。PLS-LDA是一种具有一定预测精度和可解释性的模型,因此常用于化学计量数据分析。作者还考虑了Logistic回归,一个非常简单的神经网络,没有隐藏层,输入通过一个激活函数 ϕ(t)=11+et\phi(t)=\frac{1}{1+e^{-t}} 直接传递到输出层。因此,将CNNVS与Logistic回归进行比较,可以研究使用卷积隐层的有效性。最后,作者还使用了kNN,一个不需要训练的简单方法。

    作者进行了两个系列的实验:

    1. 直接对原始数据应用不同的分类方法(即不进行预处理)
    2. 在应用分类方法之前进行数据预处理。为此,作者按照以往的一般程序对数据进行预处理。对于每一种分类方法,都使用内部交叉验证来找到应用于数据的最优预处理方法(或方法组合)3.3节会进行详细的描述。

    3.1 数据集

    该文章考虑了以下振动光谱数据集:

    • 啤酒数据集包括Rochefort 8(类1)和Rochefort 10(类2)使用三种不同类型的光谱:傅里叶变换红外(FT-IR),近红外(NIR)和拉曼。
    • 葡萄酒数据集(FT-IR),其中四个类别代表不同的地理区域来源
    • 药片数据集,采用近红外(NIR)和拉曼光谱(Raman),将样品分为4种不同类型的片剂,其活性物质含量不同
    • 咖啡数据集,包含阿拉比卡咖啡(1类)和罗布斯塔咖啡(2类)。该数据集中的光谱被截断为800-2000 cm1cm^{-1}
    • 橄榄油数据集(FT-IR)包含来自4个不同国家(希腊、意大利、葡萄牙和西班牙)的4个不同类别的样品。该数据集中的光谱被截断到799-1897 cm1cm^{-1}
    • 果汁数据集包含草莓果汁(1类)和非草莓果汁(2类)光谱截断至899-1802 cm1cm^{-1}
    • 肉类数据集包含鸡肉(1类)猪肉(2类)和火鸡肉(3类)光谱被截断至1000-1800 cm1cm^{-1}

    下表对不同数据集的特征进行了概述。对于片剂、咖啡、肉类和橄榄油数据集,在没有预先定义的测试集的情况下,作者构建了一个训练集,包含随机选择的67%的样本,其余33%的样本作为测试集。
    在这里插入图片描述

    3.2 用于比较的其他分类技术

    作者采用了标准偏最小二乘线性判别分析(PLS-LDA)方法,逻辑回归(LogReg)和k近邻(kNN)

    3.2.1 PLS-LDA

    偏最小二乘是一种回归方法,其目的是将一个包含n个样本和p个变量的数据集表示为一个潜在变量 T=[t1,t2,...ta]T=[t_1,t_2,...t_a] 的空间,方向与响应既有高方差又有高相关性。然后使用这样一组n个响应表示来拟合所有样本。这与主成分回归(PCR)形成了鲜明的对比,后者只寻找那些能使方差最大化的潜在变量。已有研究表明,当样本的组内变异性大于组间变异性时,以及需要减少变量时,PLS才是有用和有效的。这是许多化学计量数据集的情况,因为它们通常包含许多变量和相对较少的样本。

    3.2.2 Logistic回归

    Logistic回归可以看作是CNN的最后一层,换句话说,它是一个非常简单的神经网络,没有隐藏层。因此,将CNN与包含L2正则的逻辑回归进行比较,可以直接研究卷积层的重要性。分类目标函数由交叉熵误差损失和L2正则项组成:
    在这里插入图片描述
    其中,yn^ψ(wxn)\hat{y_n} \equiv \psi(\mathbf{w}\cdot\mathbf{x}_n)ψ()\psi(\cdot)为激活函数,xnx_n 为第n个样本,ww为输出权值,yny_n为目标标签。换句话说,这是一个简单的网络(需要调优的参数很少)因此它提供了一个可以与CNN进行比较的基线。使用RGS-CV网络进行学习的超参数为:

    • 正则项系数 λ1:10n\lambda_1:10^n,其中 n[3,3]n\in[-3,3]
    • 学习率 10n10^n,其中 n[8,1]n\in[-8,1]

    训练过程中使用SGD更新权重。

    3.2.3 k近邻

    k近邻(kNN)是一种非常简单而著名的分类方法。新样本的分类方法是在特征空间中选择属于k个最近邻的最频繁的类。作者用来评估样本之间距离的度量标准是曼哈坦距离和欧氏距离。考虑了 k[3,10]k\in[3,10],如前所述,k的最佳值与最佳度量方法使用RGS-CV进行选择。

    3.3 数据预处理

    光谱数据通常包含多个数据伪影。这些数据伪影表示与研究样本无关的数据中的变化。对于振动光谱数据,最常见的数据伪影是基线、光散射效应和仪器噪声。这种变化与响应变量无关,因此通常在实际数据分析之前需要通过数据预处理将其从数据中删除。作者采用了一种顺序的数据预处理方法,对数据连续应用了一系列预处理方法。这种连续应用的预处理方法的选择称为预处理策略。作者的预处理策略包括四个预处理步骤,每个步骤都与一些可能的方法相关联:

    1. 基线校正处理倾斜或变化基线的频谱:
      多项式去趋势(PolDetr):多项式阶数 [2,4]\in[2,4]
      非对称最小二乘基线估计(AsLS)
      一/二阶导(deriv1/deriv2)
    2. 散射校正,以处理不同的光散射效应:
      均值,中位数,最大值缩放(Mean/Median/MaxScaling)
      L2范数(L2)
      标准正态变量变换(SNV)
      鲁棒正态变量变换(RNV)
      多元散射校正(MSC)
    3. 消除噪声以平滑光谱:Savitsky-Golay(SavGol)(多项式阶数[2,4]\in[2,4]窗口宽度{5,9,11}px\in\{5,9,11\}px
    4. 缩放:
      meancenter(MeanCent)
      Auto/Level/Logarithm/Range/Pareto/Poisson缩放
      对数变换(LogTrans)

    注意,方法的顺序与这里给出的顺序是固定的,因为这是最常用的预处理方法顺序。meancenter总是应用于数据。

    数据集预处理方法的最佳组合可能取决于所考虑的分类模型的类型。因此,对于每一个数据集和方法,都选择了最佳的预处理策略。考虑了上述方法的所有组合(710107=49007*10*10*7=4900 种组合)选择了对训练数据进行十折交叉验证精度最高的组合。下表总结了CNN的预处理策略。
    在这里插入图片描述

    (四)结果

    下表展示了测试集中分类方法的精度:对于每一种方法,选择使用交叉验证获得最高精度的模型。
    在这里插入图片描述
    所述结果为分类方法在预处理数据(pre)和原始数据(raw)上的应用。使用3.3中描述的方法得到方法与数据集每次组合所采用的预处理策略。

    4.1 原始数据上的结果

    在非预处理数据上,CNNVS的准确率显著高于PLS-LDA (p值<0.001)平均提高24%。它也比Logistic回归(p值<0.001)有6%的平均改善。除了Raman tablet数据集,CNNVS总是优于逻辑回归。观察这个数据集与其他数据集的差异,在学习曲线、学习卷积核或原始光谱(在本例中与其他拉曼光谱进行了比较)方面没有发现大的差异。

    4.2 预处理数据上的结果

    预处理数据的结果表明,与非预处理数据相比,PLS-LDA的性能得到了提高,平均准确率提高了27%。在这种情况下,CNNVS比PLS-LDA(p值<0.001)和Logistic回归(p值<0.001)具有更好的准确性。PLS-LDA的平均精度仍然低于CNNVS(比CNNVS低7%)在考虑的大多数数据集中,有一个预处理策略使CNNVS优于PLS-LDA。事实上,任何使用优化预处理策略的方法的性能都显著优于不使用预处理的方法。换句话说,预处理对于数据分析确实是一个有价值的补充,不管使用什么数据分析方法。

    4.3 讨论

    结果表明,与PLS-LDA相比,CNNVS对预处理的依赖性较小。为了进一步证实这个结果,使用直方图比较了CNN和PLS-LDA的许多不同预处理策略的准确性。在Beers数据集上的结果如下图所示。直方图显示,PLS-LDA原始数据的精度与预处理可能达到的最佳精度相差甚远,而CNN的这种差异较小。此外,CNN获得的精度值范围小于PLS-LDA。这说明CNN对数据预处理的依赖程度比PLS-LDA低。
    在这里插入图片描述
    然而,适当的预处理仍然能够提高CNN模型的精度。对于Logistic回归和CNN来说,预处理在大多数情况下是适得其反的(即导致精度低于原始数据)这说明选择合适的预处理方法是数据分析的一个重要步骤。在其他数据集中也观察到类似的趋势。kNN对原始数据执行得相当好,但它似乎没有从特别的预处理中获益。

    作者还研究了CNNVS学习到的核,以便可能将它们与众所周知的预处理方法联系起来。虽然没有进行全面的调查,但至少给出一个解释学习到的核的示例是有趣的。例如,CNNVS对beer非预处理数据集学习到的非零元素数量较多(见下图)说明它们在进行一种平滑处理,而它们的线性趋势表明它们在执行正向和反向求导。
    在这里插入图片描述
    模型选择方法在交叉验证的基础上为卷积层选择了两个核,以达到最高的精度。注意,这两个核并不是冗余的,因为在卷积层之后需要对非线性进行校正。因此,第一个核在强度上升时输出非零值,而第二个核在强度下降时输出非零值。

    如果考虑使用预处理数据学习到的核,情况就不那么清楚了,CNNVS似乎也学习了与更高阶导数对应的核。模型选择方法为卷积层选择了四个内核,而不是像非预处理数据一样选择两个核。这表明,使用优化设计的预处理可以让CNNVS在不过拟合的情况下使用更多的核,从而达到更高的精度。

    CNNVS学习其他数据集的核也表现出类似的行为,只有少数例外,这表明对于检查过的非预处理数据,需要一个平滑派生核。

    (五)重要光谱区域

    在化学计量学数据分析中,模型解释是一个非常重要的方面。因此,识别出与样本的鉴别化学性质相对应的重要光谱区域是至关重要的,例如物质的存在或其浓度。在接下来的部分中,将比较CNNVSfs在进行最佳预处理和不进行预处理的情况下检测到的重要区域。

    关于beers和tablets的一些重要光谱区域的数据集已其他文献中进行过研究。因此,作者也对这些数据集,利用CNNVSfs来研究寻找重要的光谱区域。

    5.1 FTIR beers 数据集

    已有文献表明这一数据集最重要的光谱区域在1000-1200 cm1cm^{-1}

    CNNVSfs在非预处理数据集上的应用结果识别出的光谱区域为1000-1200 cm1cm^{-1}(见下图右侧)CNNVSfs在预处理数据集上的应用识别出光谱区域有两个,其中包括一个在 1000和2000 cm1cm^{-1}中间的一个区域(见下图左侧)因此,使用预处理或非预处理输入光谱训练的CNNVSfs都突出了重要区域的一部分。
    在这里插入图片描述

    5.2 Tablets 数据集

    在未经预处理的NIR Tablets数据集上,CNNVSfs在7700、8850、9500和10500 cm1cm^{-1} 左右的波数处识别到不同的光谱区域(见下图右)特别是在波数为 8830 cm1cm^{-1} 的区域,含有已知活性物质的峰值。该峰也由CNNVSfs在预处理后的输入光谱上得到了体现(见下图左)。因此,对于这个数据集,使用预处理或非预处理输入光谱训练的CNNVSfs都突出了重要的区域。
    在这里插入图片描述
    在Raman Tablets数据集中,重要区域来自于活性物质的存在,包括源于活性物质中氰化物(CNC\equiv N)基团的波数为 2233 cm1cm^{-1}的峰,在Tablets光谱中可以看到来自活性物质的几个其他峰(如波数为1614和3075 cm1cm^{-1})以往在这个数据集上使用的PLS分类方法只达到了较低的准确性,因此,以往没有进一步研究识别出的重要光谱区域。

    在未进行预处理的数据集中,CNNVSfs在波数约 350、1700和3100 cm1cm^{-1} 之间选取光谱区域(见下图右)从而识别出活性物质的峰,但CNNVSfs认为主峰在波数2233 cm1cm^{-1} 处并不重要。在预处理数据集上,CNNVSfs选择了2233(活性物质的主峰,见下图左)和1900 cm1cm^{-1} 左右的波数之间的光谱区域。因此,在这种情况下,对输入光谱进行的最优预处理有助于识别出重要区域。
    在这里插入图片描述

    (六)结论

    作者设计了一种简单的基于CNN的振动光谱数据分类方法,称为CNNVS,并展示了如何增强它以选择重要的光谱区域。与PLS-LDA和Logistic回归相比,CNNVS在未进行预处理和预处理数据上的应用都具有更好的精度。结果表明,与标准的PLS-LDA方法相比,CNNVS对预处理的依赖性较小,对预处理数据的处理效果较好。需要更多的振动光谱分类数据集,看看这些强结果是否也适用于不同的设置。

    一般说来,振动光谱数据具有相对较少的样本和较多的特征。为了提高模型的精度,增加数据大小的一种技术是数据增强,即添加扰动样本。该技术在CNN图像分析中得到了广泛的应用。然而,对于振动光谱数据,它并没有产生有益的影响。初步实验表明,这可能是由于不同的数据需要不同的预处理策略组合,在这种情况下很难对噪声进行建模。

    学习到的CNN核函数执行一种平滑和导数滤波。这将意味着这些过滤器不需要作为单独的预处理步骤,从而简化了最佳预处理方法的选择。如果CNNVS真的能够通过如此简单的预处理选择始终达到类似的精度,这将是一件有趣的事情。

    本文研究了振动光谱数据的分类问题。未来可以考虑使用回归方法,将研究扩展到振动光谱数据的成分分析,这将是有意思的研究。

    更详细的实验结果请查看该文章的支撑数据

    展开全文
  • 超硬核!数据结构学霸笔记,考试面试吹牛就靠它

    万次阅读 多人点赞 2021-03-26 11:11:21
    上次发操作系统笔记,很快浏览上万,这次数据结构比上次硬核的多哦,同样的会发超硬核代码,关注吧。

    上次发操作系统笔记,很快浏览上万,这次数据结构比上次硬核的多哦,同样的会发超硬核代码,关注吧。

    超硬核!操作系统学霸笔记,考试复习面试全靠它

     


     


     

    第一次笔记(复习c,课程概述)

    第一节课复习了c语言的一些知识,并简单介绍了数据结构这门课程。

     

    1、引用和函数调用:

    1.1引用:对一个数据建立一个“引用”,他的作用是为一个变量起一个别名。这是C++对C语言的一个重要补充。

    用法很简单:

    int a = 5;

    int &b = a;

    b是a别名,b与a代表的是同一个变量,占内存中同一个存储单元,具有同一地址。

    注意事项:

    1. 声明一个引用,同时必须初始化,及声明它代表哪一个变量。(作为函数参数时不需要初始化)
       
    2. 在声明一个引用后,不能再作为另一变量的引用。

         3。不能建立引用数组。

    1.2函数调用:

    其实还是通过函数来理解了一下引用

    void Myswap1(int a,int b)
    {
    	int c = a;
    	a = b;
    	b = c;
    }
    
    void Myswap2(int &a,int &b)
    {
    	int c = a;
    	a = b;
    	b = c;
    }
    
    void Myswap3(int *pa,int *pb)
    {
    	int c = *pa;
    	*pa = *pb;
    	*pb = c;
    }

    这三个函数很简单,第一个只传了值进来,不改变全局变量;而第三个也很熟悉,就是传递了地址,方便操作。依旧是“值传递”的方式,只不过传递的是变量的地址而已;那二就彻底改变了这些东西,引用作为函数参数,传入的实参就是变量,而不是数值,真正意义上的“变量传递”。

     

    2、数组和指针:

    这一块讲得比较简单,就是基本知识。

    主要内容:

    1、函数传数组就是传了个指针,这个大家都知道,所以传的时候你写arr[],里面写多少,或者不写,都是没关系的,那你后面一定要放一个变量来把数组长度传进来。

    2、还有就是,定义:int arr[5],你访问越界是不会报错的,但是逻辑上肯定就没有道理了。那用typedef int INTARR[3];访问越界,在vs上会报错,要注意。

    3、再说一下指针和数组名字到底有什么区别?这本来就是两个东西,可能大家没注意罢了。

    第一:指针可以自增,数组名不行,因为是常量啊。

    第二:地址不同,虽然名字[n],都可以这样用,但是数组名地址就是第一个元素地址。指针地址就是那个指针的地址,指针里存的才是第一个元素地址。

    第三:sizeof(),空间不一样,数组是占数组那么大空间。指针是四个字节。

    本来就是俩东西,这么多不同都是本质不同的体现。

    3、结构体:

    也是讲的基本操作,基本就是这个东西:

    typedef struct Date
    {
    	int Year;
    	int Month;
    	int Day;
    	struct Date *pDate;
    }Date, *pDate;

    1、内部无指向自己的指针才可以第一行先不起名字。

    2、内部不能定义自己的,如果能的话那空间不就无限了么。很简单的逻辑

     

    指针我不习惯,还是写Date *比较顺眼

    3、有同学没注意:访问结构体里的东西怎么访问?

    Date.这种形式,或者有指向这个节点的指针p可以p->这种形式,别写错了。

     

    4、还有就是结构体能直接这么赋值:

       Date d1 = {2018,9,11};

    我竟然不知道,以前还傻乎乎的一个一个赋值呢。

     

    5、还有,想写一下结构体有什么优点。。

    这一块可能写的就不好了,因为不是老师讲的。。

    比如学生成绩,如果不用结构体,我们一个学生可能有十几个信息,那定义变量和操作就很烦琐了,数据结构有一种松散的感觉。用一个结构体来表示更好,无论是程序的可读性还是可移植性还是可维护性,都得到提升。还有就是函数调用的时候,传入相当多的参数,又想操作或者返回,那是很麻烦的事。现在只传入一个结构体就好了,操作极其简单。总结一下就是好操作,中看中用,有机地组织了对象的属性。以修改结构体成员变量的方法代替了函数(入口参数)的重新定义。

    基本就这样吧。

    6、还有就是它了:typedef int INTARR[3];这样直接定义了一个数据类型,长度为3的数组,然后直接这样用就可以了:

    INTARR arr1;

     

    回忆完C语言储备知识,然后讲了数据结构的基本概念

     

    数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。

    数据:是对客观事物的符号表示,在计算机中指能输入到计算机中并被处理的符号总称。

    数据元素:数据的基本单位

    数据项:数据的不可分割的最小单位

    数据对象:性质相同的数据元素的集合。

    举例:动物是数据,某只动物是数据元素,猫狗是数据对象,颜色可以是数据项。

     

    数据元素之间存在某种关系,这种关系成为结构。

    四种基本结构:

    集合:除了同属一个集合无其他关系。

    线性结构:一对一的关系

    树形结构:一对多的关系

    图状结构:多对多的关系

     

    第二次笔记(基本概念,时间空间复杂度)

    今天继续说明了一些基本概念,讲解了时间空间复杂度。

    (对于概念的掌握也很重要)

     

    元素之间的关系在计算机中有两种表示方法:顺序映像和非顺序映像,由此得到两种不同的储存结构:

    顺序存储结构和链式存储结构。

     

    顺序:根据元素在存储器中的相对位置表示关系

    链式:借助指针表示关系

     

    数据类型:是一个值的集合和定义在这个值集上的一组操作的总称。

    抽象数据类型:是指一个数学模型以及定义在该模型上的一组操作。(仅仅取决于逻辑特性,与其在计算机内部如何表示和实现无关)

     

    定义抽象数据类型的一种格式:

    ADT name{

    数据对象:<>

    数据关系:<>

    基本操作:<>

    }ADT name

     

    算法:是对特定问题求解步骤的一种描述。

    算法五个特性:

    1. 有穷性:有穷的时间内完成,或者可以说是可接受的时间完成
    2. 确定性:对于相同的输入只能得到相同的输出
    3. 可行性:描述的操作都可以执行基本操作有限次来实现
    4. 输入:零个或多个输入。取自于某个特定对象的集合
    5. 输出:一个或多个输出

     

    设计要求:正确性、可读性、健壮性、效率与低存储量需求。

    执行频度概念:是指通过统计算法的每一条指令的执行次数得到的。

    执行频度=算法中每一条语句执行次数的和

    一般认定每条语句执行一次所需时间为单位时间(常数时间)O(1)

     

    几个小知识和小问题:

    1)循环执行次数n+1次,不是n次。第一次执行i=1和判断i<=n以后执行n次判断和i++。所以该语句执行了n+1次。在他的控制下,循环体执行了n次。

    2)四个并列程序段:分别为O(N),O(N^2),O(N^3),O(N*logN),整个程序时间复杂度为O(N^3),因为随着N的增长,其它项都会忽略

    3)算法分析的目的是分析算法的效率以求改进

    4)对算法分析的前提是算法必须正确

    5)原地工作指的不是不需要空间,而是空间复杂度O(1),可能会需要有限几个变量。

    实现统一功能两种算法:时间O(2^N),O(N^10),假设计算机可以运行10^7秒,每秒可执行基本操作10^5次,问可解问题规模各为多少?选哪个更为合适?

    计算机一共可执行10^7*10^5=10^12次

    第一种:n=log2,(10^12)=12log(2,10)

    第二种:n=(10^12)^0.1

    显然1更适用。

    虽然一般情况多项式算法比指数阶更优

     

    时间空间复杂度概述

     

    找个时间写一写时间复杂度和一些问题分类,也普及一下这方面知识。

    如何衡量一个算法好坏

    很显然,最重要的两个指标:需要多久可以解决问题、解决问题耗费了多少资源

    那我们首先说第一个问题,要多长时间来解决某个问题。那我们可以在电脑上真实的测试一下嘛,多种方法比一比,用时最少的就是最优的啦。

    但是没必要,我们可以通过分析计算来确定一个方法的好坏,用O()表示,括号内填入N、1,等式子。

    这到底是什么意思呢?

    简单来说,就是这个方法,时间随着数据规模变化而增加的快慢。时间可以当成Y,数据规模是X,y=f(x),就这样而已。但是f(x)不是准确的,只是一个大致关系,y=10x,我们也视作x,因为他的增长速度还是n级别的。现在就可以理解了:一般O(N)就是对每个对象访问优先次数而已。请注意:O(1)它不是每个元素访问一次,而是Y=1的感觉,y不随x变化而变化,数据多大它的时间是不变的,有限的常数操作即可完成。

    那我们就引入正规概念:

    时间复杂度是同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。

    计算机科学中,算法的时间复杂度是一个函数,它定性描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,它考察当输入值大小趋近无穷时的情况。

    注意:文中提到:不包括这个函数的低阶项和首项系数。什么意思呢?就是说10n,100n,哪怕1000000000n,还是算做O(N),而低阶项是什么意思?不知大家有没有学高等数学1,里面有最高阶无穷大,就是这个意思。举个例子。比如y=n*n*n+n*n+n+1000

    就算做o(n*n*n),因为增长速率最大,N*N及其它项增长速率慢,是低阶无穷大,n无限大时,忽略不计。

     

    那接着写:o(n*n*n)的算法一定不如o(n)的算法吗?也不一定,因为之前说了,时间复杂度忽略了系数,什么意思?o(n)可以是10000000n,当n很小的时候,前者明显占优。

    所以算法要视实际情况而定。

    算法的时间 复杂度常见的有:
    常数阶 O(1),对数阶 O(log n),线性阶 O(n),
    线性对数阶 O(nlog n),平方阶 O(n^2),立方阶 O(n^3),…,
    k 次方阶O(n^k),指数阶 O(2^n),阶乘阶 O(n!)。

    常见的算法的时间 复杂度之间的关系为:
           O(1)<O(log n)<O(n)<O(nlog n)<O(n^2)<O(2^n)<O(n!)<O(n^n) 

     

    我们在竞赛当中,看见一道题,第一件事就应该是根据数据量估计时间复杂度。

    计算机计算速度可以视作10^9,如果数据量是10000,你的算法是O(N*N),那就很玄,10000*10000=10000 0000,别忘了还有常数项,这种算法只有操作比较简单才可能通过。你可以想一想O(nlog n)的算法一般就比较稳了。那数据量1000,一般O(N*N)就差不多了,数据量更小就可以用复杂度更高的算法。大概就这样估算。

     

    当 n 很大时,指数阶算法和多项式阶算法在所需时间上非常
    悬殊。因此,只要有人能将现有指数阶算法中的任何一个算法化
    简为多项式阶算法,那就取得了一个伟大的成就。

    体会一下:

     

    空间复杂度也是一样,用来描述占空间的多少。

    注意时间空间都不能炸。

    所以才发明了那么多算法。

    符上排序算法的时间空间表,体会一下:

     


    排序博客:加深对时间空间复杂度理解

    https://blog.csdn.net/hebtu666/article/details/81434236

     

     

     

    引入:算法优化

     

    想写一系列文章,总结一些题目,看看解决问题、优化方法的过程到底是什么样子的。

     

    系列问题一:斐波那契数列问题

    在数学上,斐波纳契数列以如下被以递归的方法定义:F(0)=0,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)根据定义,前十项为1, 1, 2, 3, 5, 8, 13, 21, 34, 55

    问题一:

    给定一个正整数n,求出斐波那契数列第n项(这时n较小)

    解法一:最笨,效率最低,暴力递归,完全是抄定义。请看代码

    def f0(n):
        if n==1 or n==2:
            return 1
        return f(n-1)+f(n-2)

     

    分析一下,为什么说递归效率很低呢?咱们来试着运行一下就知道了

     

    比如想求f(10),计算机里怎么运行的?

     

    每次要计算的函数量都是指数型增长,时间复杂度O(2^(N/2))<=T(N)<=O(2^N),效率很低。效率低的原因是,进行了大量重复计算,比如图中的f(8),f(7).....等等,你会发现f(8)其实早就算过了,但是你后来又要算一遍。

    如果我们把计算的结果全都保存下来,按照一定的顺序推出n项,就可以提升效率, 斐波那契(所有的一维)的顺序已经很明显了,就是依次往下求。。

    优化1

    def f1(n):
        if n==1 or n==2:
            return 1
        l=[0]*n
        l[0],l[1]=1,1
        for i in range(2,n):
            l[i]=l[i-1]+l[i-2]
        return l[-1]

     

    时间复杂度o(n),依次往下打表就行,空间o(n).

    继续优化:既然只求第n项,而斐波那契又严格依赖前两项,那我们何必记录那么多值呢?记录前两项就行了

     

    优化2

    def f2(n):
        a,b=1,1
        for i in range(n-1):
            a,b=b,a+b
        return a

    此次优化做到了时间o(n),空间o(1)

    附:这道题掌握到这里就可以了,但是其实有时间o(log2n)的方法

     

    优化三:

    学习过线性代数的同学们能够理解:

     

    结合快速幂算法,我们可以在o(log2n)内求出某个对象的n次幂。(在其它专题详细讲解)

    注意:只有递归式不变,才可以用矩阵+快速幂的方法。

    注:优化三可能只有在面试上或竞赛中用,当然,快速幂还是要掌握的。

     

    经过这个最简单的斐波那契,大家有没有一点感觉了?

    好,我们继续往下走

    (补充:pat、蓝桥杯原题,让求到一万多位,结果模一个数。

    可以每个结果都对这个数取模,否则爆内存,另:对于有多组输入并且所求结果类似的题,可以先求出所有结果存起来,然后直接接受每一个元素,在表中查找相应答案)

     

    问题二:

    一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

    依旧是找递推关系,分析:跳一阶,就一种方法,跳两阶,它可以一次跳两个,也可以一个一个跳,所以有两种,三个及三个以上,假设为n阶,青蛙可以是跳一阶来到这里,或者跳两阶来到这里,只有这两种方法。它跳一阶来到这里,说明它上一次跳到n-1阶,同理,它也可以从n-2跳过来,f(n)为跳到n的方法数,所以,f(n)=f(n-1)+f(n-2)

    和上题的优化二类似。

    因为递推式没变过,所以可以用优化三

     

    问题三:

    我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?提示,大矩形看做是长度吧

    请读者自己先思考一下吧。。。仔细看题。。仔细思考

    如果n是1,只有一种,竖着放呗;n是2,两种:

    n等于3,三种:

     

    题意应该理解了吧?

    读到这里,你们应该能很快想到,依旧是斐波那契式递归啊。

    对于n>=3:怎么能覆盖到三?只有两种办法,从n-1的地方竖着放了一块,或者从n-2的位置横着放了两块呗。

    和问题二的代码都不用变。

     

    问题四:

    给定一个由0-9组成的字符串,1可以转化成A,2可以转化成B。依此类推。。25可以转化成Y,26可以转化成z,给一个字符串,返回能转化的字母串的有几种?

    比如:123,可以转化成

    1 2 3变成ABC,

    12 3变成LC,

    1 23变成AW,三种,返回三,

    99999,就一种:iiiii,返回一。

    分析:求i位置及之前字符能转化多少种。

    两种转化方法,一,字符i自己转换成自己对应的字母,二,和前面那个数组成两位数,然后转换成对应的字母

    假设遍历到i位置,判断i-1位置和i位置组成的两位数是否大于26,大于就没有第二种方法,f(i)=f(i-1),想反,等于f(i-1)+f(i-2)

    注意:递归式不确定,不能用矩阵快速幂

     

    (这几个题放到这里就是为了锻炼找递归关系的能力,不要只会那个烂大街的斐波那契)

     

    '''
    斐波那契问题:
    斐波纳契数列以如下被以递归的方法定义:
    F(1)=1
    F(2)=1
    F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
    '''
    #暴力抄定义,过多重复计算
    def f0(n):
        if n==1 or n==2:
            return 1
        return f(n-1)+f(n-2)
    #记录结果后依次递推的优化,时间O(N)
    def f1(n):
        if n==1 or n==2:
            return 1
        l=[0]*n
        l[0],l[1]=1,1
        for i in range(2,n):
            l[i]=l[i-1]+l[i-2]
        return l[-1]
    #既然严格依赖前两项,不必记录每一个结果,优化空间到O(1)
    def f2(n):
        a,b=1,1
        for i in range(n-1):
            a,b=b,a+b
        return a

    第三次笔记(线性表结构和顺序表示)

     

    这节课介绍了线性表结构和顺序表示的一部分内容。

    操作太多,而且书上有,就不一一介绍分析了。

    线性表定义:n个数据元素的有限序列。

    特点:

    1. 存在唯一一个称作“第一个”的元素。
    2. 存在唯一一个称作“最后一个”的元素
    3. 除最后一个元素外,集合中每一个元素都只有一个直接前趋
    4. 除最后一个元素外,集合中每一个元素都只有一个直接后继

    注意1、2条:证明循环的序列不是线性表

     

    注意:

    1)线性表从1开始,线性表第一个元素对应到数组中下标是0.

    2)函数通过引用对线性表的元素进行修改即可

    3)数组比较特别,它即可视为逻辑结构,又可视为存储结构。

    4)每一个表元素都是不可再分的原子数据。一维数组可以视为线性表,二维数组不可以,在逻辑上它最多可以有两个直接前趋和直接后继。

    5)线性表具有逻辑上的顺序性,在序列中各元素有其先后次序,各个数据元素在线性表中的逻辑位置只取决于序号。

     

    顺序表:是线性表的循序储存结构,以物理位置表示逻辑关系,任意元素可以随机存取。用一组地址连续的存储单元依次存储线性表中的元素。逻辑顺序和物理顺序是一致的。可以顺序访问,也可随机访问。

    顺序表存储:

    这些定式还是很重要的,比如define typedef等,真正实现时最好就这样写,不要自己规定个长度和数据类型,这样以后好维护、修改。

    静态存储分配:

    #define maxSize 100//显式定义表长

    Typedef int DataType;//定义数据类型

    Typedef struct{

    DataType data[maxSize];//静态分配存储表元素向量

    Int n;//实际表中个数

    }SeqList;

     

    动态存储分配:

    #define maxSize 100//长度初始定义

    Typedef int DataType;//定义数据类型

    Typedef struct{

    DataType *data;//动态分配数组指针

    Int maxSize,n;//最大容量和当前个数

    }SeqList;

     

    初始动态分配:

    Data=(DataType *)malloc(sizeof(DataType)* initSize);

    C++:data=new DataType[initSize];

    maxSize=initSize;n=0;

    动态分配存储,向量的存储空间是在程序执行过程中通过动态存储分配来获取的。空间满了就另外分配一块新的更大的空间,用来代替原来的存储空间,从而达到扩充的目的。

     

    插入:需要查找,移动元素,概率上1,2,3....n,平均O(N)

    删除:同样需要移动元素。填充被空出来的存储单元。

    在等概率下平均移动次数分别为n/2,(n-1)/2

     插入注意事项:

    1. 需要判断是否已满
    2. 要从后向前移动,否则会冲掉元素

    删除注意事项:

    1. 需要先判断是否已空
    2. 需要把后方元素前移,要从前向后。

     

    注意:线性表的顺序存储借用了一维数组,但是二者不同:

    1. 一维数组各非空结点可以不相继存放,但顺序表是相继存放的
    2. 顺序表长度是可变的,一维数组长度是确定的,一旦分配就不可变
    3. 一维数组只能按下标存取元素,顺序表可以有线性表的所有操作。

     

     

    第四次笔记(链表概述)

     

    介绍了链表和基本操作

    用一组物理位置任意的存储单元来存放线性表的数据元素。 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。因此,链表中元素的逻辑次序和 物理次序不一定相同。

     

    定义:

    typedef  struct  Lnode{  
            //声明结点的类型和指向结点的指针类型  
            ElemType         data;    //数据元素的类型 
            struct   Lnode  *next;   //指示结点地址的指针  
    }Lnode, *LinkList;  
    struct Student
    { char num[8];   //数据域
      char name[8];  //数据域
      int score;          //数据域
      struct Student *next;  // next 既是 struct Student 
                                           // 类型中的一个成员,又指 
                                           // 向 struct Student 类型的数据。 
    }Stu_1, Stu_2, Stu_3, *LL;  
    

    头结点:在单链表的第一个结点之前人为地附设的一个结点。

    带头结点操作会方便很多。

    带和不带的我都写过了

    下面列出我见过的一些好题

    1、

    线性表的每个结点只能是一个简单类型,而链表的每个结点可以是一个复杂类型。

    • 正确
    • 错误

     

    错,线性表是逻辑结构概念,可以顺序存储或链式储,与元素数据类型无关。链表就是线性表的一种  前后矛盾

     

    2、

    若某线性表中最常用的操作是在最后一个元素之后插入一个元素和删除第一个元素,则采用(    )存储方式最节省运算时间。

    • 单链表
    • 仅有头指针的单循环链表
    • 双链表
    • 仅有尾指针的单循环链表

      对于A,B,C要想在尾端插入结点,需要遍历整个链表。

      对于D,要插入结点,只要改变一下指针即可,要删除头结点,只要删除指针.next的元素即可。

     

    3、注意:线性表是具有n个数据元素的有限序列,而不是数据项

     

    4、

    以下关于单向链表说法正确的是

    • 如果两个单向链表相交,那他们的尾结点一定相同
    • 快慢指针是判断一个单向链表有没有环的一种方法
    • 有环的单向链表跟无环的单向链表不可能相交
    • 如果两个单向链表相交,那这两个链表都一定不存在环

    自己多画画想想就明白了,关于快慢指针我以后会写总结。

     

    5、

    链接线性表是顺序存取的线性表 。 ( )

    • 正确
    • 错误

    链接线性表可以理解为链表
    线性表分为顺序表和链表
    顺序表是顺序存储结构、随机存取结构
    链表是随机存储结构、顺序存取结构

     

    6、

    typedef struct node_s{
        int item;
        struct node_s* next;
    }node_t;
    node_t* reverse_list(node_t* head)
    {
        node_t* n=head;
        head=NULL;
        while(n){
        _________               
        }
        return head;
     }

    空白处填入以下哪项能实现该函数的功能?

    • node_t* m=head; head=n; head->next=m; n=n->next;
    • node_t* m=n; n=n->next; m->next=head; head=m;
    • node_t* m=n->next; n->next=head; n=m; head=n;
    • head=n->next; head->next=n; n=n->next;


     

    代码的功能是要实现链表的反转。为了方便阐述,每个结点用①②③④⑤⑥等来标示。

    在执行while(n)循环之前,有两句代码:

    node_t* n=head;

    head=NULL;

    这两行代码的中:第一句的作用是用一个临时变量n来保存结点①,第二句是把head修改为NULL。

    然后就开始遍历了,我们看到while循环里的那四句代码:

    node_t* m=n; 
    n=n->next; 
    m->next=head; 
    head=m;
    

    先看前两句,用m来保存n,然后让n指向n的下一个结点,之所以复制 n 给 m ,是因为 n 的作用其实是  控制while循环次数  的作用,每循环一次它就要被修改为指向下一个结点。

    再看后两句,变量head在这里像是一个临时变量,后两句让 m 指向了 head,然后 head 等于 m。

     

    7、

    若某表最常用的操作是在最后一个结点之后插入一个节点或删除最后一二个结点,则采用()省运算时间。

    • 单链表
    • 双链表
    • 单循环链表
    • 带头结点的双循环链表

    D

    带头结点的双向循环链表,头结点的前驱即可找到最后一个结点,可以快速插入,再向前可以找到最后一二个结点快速删除

    单链表找到链表尾部需要扫描整个链表

    双链表找到链表尾部也需要扫描整个链表

    单循环链表只有单向指针,找到链表尾部也需要扫描整个链表

     

    8、

    单链表的存储密度(  )

    • 大于1
    • 等于1
    • 小于1
    • 不能确定

    全麦白

    存储密度 = 数据项所占空间 / 结点所占空间

     

    9、完成在双循环链表结点p之后插入s的操作是

    • s->prior=p; s->next=p->next; p->next->prior=s ; p->next=s;

     

    10、用不带头结点的单链表存储队列,其队头指针指向队头结点,队尾指针指向队尾结点,则在进行出队操作时()

    • 仅修改队头指针
    • 仅修改队尾指针
    • 队头、队尾指针都可能要修改
    • 队头、队尾指针都要修改

     

    当只有一个元素,出队列时,要将队头和队尾,指向-1.所以说队头和队尾都需要修改

     

     

    链表刷了几百道吧,好题还有很多,以后接着更新

     

     

    第六次笔记(链表选讲/静态链表)

     

    本节课介绍了单链表的操作实现细节,介绍了静态链表。

     

    链表带头的作用:对链表进行操作时,可以对空表、非空表的情况以及 对首元结点进行统一处理,编程更方便。

    下面给出带头的单链表实现思路:

     

    按下标查找:

    判断非法输入,当 1 < =i <= n 时,i 的值是合法的。

    p = L -> next; j = 1;

    while ( p && j < i ) {  p = p ->next; ++j; }

    return 

     

    按值查找:

     p = L1 -> next;

     while ( p && p ->data!=key)          p = p -> next;

    return;

     

    插入:

    判断

    查找

    创建

    插入

     

    删除:

    查找

    删除

    释放内存

     

    静态链表

    对于线性链表,也可用一维数组来进行描述。这种描述方法便于在没有指针类型的高级程序设计语言中使用链表结构。

    这种存储结构,仍需要预先分配一个较大的空间,但在作为线性表的插入和删除操作时不需移动元素,仅需修改指针,故仍具有链式存储结构的主要优点。

     

    表示:

    #define MAXSIZE 1000      / /链表的最大长度

    typedef  struct{      

        ElemType data;        

        int cur;

    }component,  SLinkList[MAXSIZE];

     

    过程:

     

     

    顺序存储线性表实现

     

    在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。

     

    顺序存储结构的主要优点是节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。但顺序存储方法的主要缺点是不便于修改,对结点的插入、删除运算时,可能要移动一系列的结点。

    优点:随机存取表中元素。缺点:插入和删除操作需要移动元素。

     

    线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)。

    给出两种基本实现:

    /*
    静态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INITSIZE 100
    #define ElemType int
    #define Status int
    #define OK     1
    #define ERROR  0
    
    typedef struct
    {
    	ElemType elem[LIST_INITSIZE];
    	int length;
    }SqList;
    
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DisCreat(SqList A,SqList *B,SqList *C);//拆分(按正负),也可以根据需求改
    //虽然思想略简单,但是要写的没有错误,还是需要锻炼coding能力的
    
    Status InitList(SqList *L)
    {
        L->length = 0;//长度为0
        return OK;
    }
    
    Status ListInsert(SqList *L, int i,ElemType e)
    {
        int j;
        if(i<1 || i>L->length+1)
            return ERROR;//判断非法输入
        if(L->length == LIST_INITSIZE)//判满
        {
            printf("表已满");//提示
            return ERROR;//返回失败
        }
        for(j = L->length;j > i-1;j--)//从后往前覆盖,注意i是从1开始
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        (L->length)++;//表长加1
        return OK;//反回成功
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i = 0;i < L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DisCreat(SqList A,SqList *B,SqList *C)
    {
        int i;
        for(i = 0;i < A.length;i++)//依次遍历A中元素
        {
            if(A.elem[i]<0)//判断
                ListInsert(B,B->length+1,A.elem[i]);//直接调用插入函数实现尾插
            else
                ListInsert(C,C->length+1,A.elem[i]);
        }
    }
    
    int main(void)
    {
        //复制的
    	SqList L;
    	SqList B, C;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,33,-3,-88,21,77,0,-9};
    	InitList(&L);
    	InitList(&B);
    	InitList(&C);
    	for (i = 1; i <= 9; i++)
    		ListInsert(&L,i,data[i-1]);
        printf("插入完成后L = : ");
    	ListPrint(L);
        ListDelete(&L,1,&e);
    	printf("删除第1个后L = : ");
    	ListPrint(L);
        DisCreat(L , &B, &C);
    	printf("拆分L后B = : ");
    	ListPrint(B);
    	printf("拆分L后C = : ");
    	ListPrint(C);
    	printf("拆分L后L = : ");
    	ListPrint(L);
    }

    静态:长度固定

    动态:不够存放可以加空间(搬家)

     

    /*
    子任务名任务:1_2 动态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INIT_SIZE 100
    #define LISTINCREMENT 10
    #define Status int
    #define OVERFLOW -1
    #define OK 1
    #define ERROR 0
    #define ElemType int
    
    typedef struct
    {
    	ElemType * elem;
    	int length;
    	int listsize;
    }SqList;
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DeleteMin(SqList *L);//删除最小
    
    Status InitList(SqList *L)
    {
        L->elem = (ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));//申请100空间
    	if(!L->elem)//申请失败
    		return ERROR;
    	L->length = 0;//长度0
    	L->listsize = LIST_INIT_SIZE;//容量100
    	return OK;//申请成功
    }
    
    Status ListInsert(SqList *L,int i,ElemType e)
    {
        int j;
        ElemType *newbase;
        if(i<1 || i>L->length+1)
            return ERROR;//非法输入
            
        if(L->length >= L->listsize)//存满了,需要更大空间
        {
            newbase = (ElemType*)realloc(L->elem,(L->listsize+LISTINCREMENT)*sizeof(ElemType));//大10的空间
            if(!newbase)//申请失败
                return ERROR;
            L->elem = newbase;//调指针
            L->listsize+= LISTINCREMENT;//新容量
        }
        
        for(j=L->length;j>i-1;j--)//从后往前覆盖
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        L->length++;//长度+1
        return OK;
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i=0;i<L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DeleteMin(SqList *L)
    {
        //表空在Listdelete函数里判断
        int i;
        int j=0;//最小值下标
        ElemType *e;
        for(i=0;i<L->length;i++)//寻找最小
        {
            if(L->elem[i] < L->elem[j])
                j=i;
        }
        ListDelete(L,j+1,&e);//调用删除,注意j要+1
    }
    
    int main(void)
    {
    	SqList L;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,-33,3,-88,21,77,0,-9};
    	InitList(&L);
    	for (i = 1; i <= 9; i++)
    	{
    		ListInsert(&L,i,data[i-1]);
    	}
    	printf("插入完成后 L = : ");
    	ListPrint(L);
        ListDelete(&L, 2, &e);
    	printf("删除第 2 个后L = : ");
    	ListPrint(L);
        DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    }

    单链表,不带头实现

    链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

    使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表双向链表以及循环链表

     

    下面给出不带头的单链表标准实现:

    定义节点:

    typedef struct node 
    { 
        int data;
        struct node * next;
    }Node;

    尾插:

    void pushBackList(Node ** list, int data) 
    { 
        Node * head = *list;
        Node * newNode = (Node *)malloc(sizeof(Node));//申请空间
        newNode->data = data; newNode->next = NULL;
        if(*list == NULL)//为空
            *list = newNode;
        else//非空
        {
            while(head ->next != NULL)
                head = head->next;
            head->next = newNode;
        }
    }
    
    

    插入:

    int insertList(Node ** list, int index, int data) 
    {
        int n;
        int size = sizeList(*list); 
        Node * head = *list; 
        Node * newNode, * temp;
        if(index<0 || index>size) return 0;//非法
        newNode = (Node *)malloc(sizeof(Node)); //创建新节点
        newNode->data = data; 
        newNode->next = NULL;
        if(index == 0) //头插
        {
            newNode->next = head; 
            *list = newNode; 
            return 1; 
        }
        for(n=1; n<index; n++) //非头插
            head = head->next;
        if(index != size) 
            newNode->next = head->next; 
        //链表尾部next不需指定
        head->next = newNode; 
        return 1;
    }
    

    按值删除:

    void deleteList(Node ** list, int data) 
    { 
        Node * head = *list; Node * temp; 
        while(head->next!=NULL) 
        { 
            if(head->next->data != data) 
            { 
                head=head->next; 
                continue; 
            } 
            temp = head->next;
            if(head->next->next == NULL) //尾节点删除
                head->next = NULL; 
            else 
                head->next = temp->next; 
            free(temp);
        }    
        head = *list; 
        if(head->data == data) //头结点删除
        { 
            temp = head; 
            *list = head->next; 
            head = head->next; 
            free(temp); 
        }
    }
    

    打印:

    void printList(Node * head) 
    { 
        Node * temp = head; 
        for(; temp != NULL; temp=temp->next) 
            printf("%d ", temp->data); 
        printf("\n"); 
    }

    清空:

    void freeList(Node ** list) 
    { 
        Node * head = *list; 
        Node * temp = NULL; 
        while(head != NULL) //依次释放
        { 
            temp = head; 
            head = head->next; 
            free(temp); 
        } 
        *list = NULL; //置空
    }

    别的也没啥了,都是基本操作

    有些代码要分情况,很麻烦,可读性较强吧

    看我压缩代码:https://blog.csdn.net/hebtu666/article/details/81261043

     

    双链表带头实现

    以前写的不带头的单链表实现,当时也啥也没学,好多东西不知道,加上一心想压缩代码,减少情况,所以写得不太好。

    请教了老师,首先是命名问题和代码紧凑性等的改进。还有可读性方面的改进,多写了一些注释。并且因为带头的比较好写,好操作,所以标准写法也不是很长,繁琐。

     

     

    下面贴代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    typedef struct node{
        int key;//数据
        struct node * prev;//前驱
        struct node * next;//后继
    }Node;

    初始化(带头) 

    Node * list;
    //初始化,这里·我们list不再是NULL,而是指向了一个节点
    //这个改进方便了很多操作,也不用借助二重指针把list和next统一表示了
    
    void init(Node * list)//初始化
    {
        list = (Node *)malloc(sizeof(Node));
        list->next = NULL;
        list->prev = NULL;
    }

    查找(不用再判断一下空不空)

    Node * find(int key,Node * list)
    {
        Node * head = list->next;//从头节点后面开始找
        while(head != NULL && head->key != key)//找到或空跳出
            head = head->next;
        return head;
    }

    打印

    void printList(Node * list)//打印
    {
        Node * temp = list->next;头节点下一个开始
        while(temp != NULL)
        {
            printf("%d ",temp->key);
            temp = temp->next;
        }
        printf("\n");
    }

    删除指定结点

    void delete(Node * list)//删除指定结点
    {
        list->prev->next = list->next;前面后面指针改了,再free自己即可
        list->next->prev = list->prev;
        free(list);
    }

    配合一下删除:

    void deleteKey(int key,Node * list)
    {
        delete(find(key,list));
    }

    头插:

    void insertHead(int key,Node * list)//头插
    {
        Node * newNode = (Node *)malloc(sizeof(Node));//初始化
        newNode->key = key;
        newNode->next = list->next;//赋值后继
        if(list->next != NULL)//如果有后继,赋值后继的前指针为新结点
            list->next->prev = newNode;
        list->next = newNode;//改头
        newNode->prev = list;//赋值新结点前指针
    }

    按下标插入

    单链表都写了,我就不写长度函数和判断非法了,用的时候注意吧。

    void insert(int key,Node * list,int index)
    {
        Node * head=list;//最后指到要插位置的前一个即可
        Node * newNode = (Node *)malloc(sizeof(Node));//初始化
        newNode->key = key;
        while(index--)
            head = head->next;
        newNode->next = head->next;//修改指针
        newNode->prev = head;
        head->next = newNode;
    }

    指定某值后插入不写了,和这个修改指针逻辑一样,再传一个find配合一下就行了。

     

    然后简单测试

    int main()
    {
        Node * list = NULL;
        init(list);
        insertHead(1,list);
        insertHead(2,list);
        insertHead(3,list);
        printList(list);
        deleteKey(2,list);
        printList(list);
        insert(10,list,0);
        printList(list);
    }

     

    第七次笔记(栈/队列)

     

    介绍栈和队列基本概念和用法。

     

    设输入序列1、2、3、4,则下述序列中( )不可能是出栈序列。【中科院中国科技大学2005】

    A. 1、2、3、4                      B. 4、 3、2、1

    C. 1、3、4、2                      D.4、1、2、3

    选D

    我是一个个模拟来做的。

     

    描述栈的基本型性质:

    1、集合性:栈是由若干个元素集合而成,没有元素(空集)成为空栈。

    2、线性:除栈顶和栈底之外,任意元素均有唯一前趋和后继。

    3、运算受限:只在一端插入删除的线性表即为栈

     

    顺序存储和顺序存取:顺序存取是只能逐个存或取结构中的元素,例如栈。顺序存储是利用一个连续的空间相继存放,例如栈可基于一维数组存放元素。

     

    一个较早入栈的元素能否在后面元素之前出栈?如果后面元素压在它上面,就不可以了。如果后面元素未压入,它可以弹出。在其他元素前面。

     

     

    栈与递归:

      当在一个函数的运行期间调用另一个函数时,在运行 该被调用函数之前,需先完成三件事:  将实参等传递给被调用函数,保存返回地址(入栈);  为被调用函数的局部变量分配存储区;    将控制转移到被调用函数的入口。  

    从被调用函数返回调用函数之前,应该完成:  保存被调函数的计算结果;  释放被调函数的数据区;  按被调函数保存的返回地址(出栈)将控制转移到调        用函数。

    多个函数嵌套调用的规则是:后调用先返回。

     此时的内存管理实行“栈式管理”

     

    队列:

            在多用户计算机系统中,各个用户需要使用 CPU 运行自己的程序,它们分别向操作系统提出使用 CPU 的请求,操作系统按照每个请求在时间上的先后顺序, 将其排成一个队列,每次把CPU分配给队头用户使用, 当相应的程序运行结束,则令其出队,再把CPU分配 给新的队头用户,直到所有用户任务处理完毕。

     

    以主机和打印机为例来说明,主机输出数据给打印 机打印,主机输出数据的速度比打印机打印的速度要快 得多,若直接把输出的数据送给打印机打印,由于速度 不匹配,显然不行。解决的方法是设置一个打印数据缓 冲区,主机把要打印的数据依此写到这个缓冲区中,写 满后就暂停输出,继而去做其它的事情,打印机就从缓 冲区中按照先进先出的原则依次取出数据并打印,打印 完后再向主机发出请求,主机接到请求后再向缓冲区写 入打印数据,这样利用队列既保证了打印数据的正确, 又使主机提高了效率。

     

    双端队列:

    某队列允许在其两端进行入队操作,但仅允许在一端进行出队操作,若元素a,b,c,d,e依次入队列后,再进行出队操作,则不可能得到的顺序是( )。 

    A. bacde                B. dbace              C. dbcae                D. ecbad

    解析:出队只能一端,所以abcde一定是这个顺序。

    反模拟入队,每次只能在两边出元素。

     

    栈/队列 互相模拟实现

     

    用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

    思路:大概这么想:用一个辅助栈把进第一个栈的元素倒一下就好了。

    比如进栈1,2,3,4,5

    第一个栈:

    5

    4

    3

    2

    1

    然后倒到第二个栈里

    1

    2

    3

    4

    5

    再倒出来,顺序为1,2,3,4,5

    实现队列

    然后要注意的事情:

    1)栈2非空不能往里面倒数,顺序就错了。栈2没数再从栈1倒。

    2)栈1要倒就一次倒完,不倒完的话,进新数也会循序不对。

    import java.util.Stack;
     
    public class Solution {
        Stack<Integer> stack1 = new Stack<Integer>();
        Stack<Integer> stack2 = new Stack<Integer>();
         
        public void push(int node) {
            stack1.push(node);
        }
         
        public int pop() {
            if(stack1.empty()&&stack2.empty()){
                throw new RuntimeException("Queue is empty!");
            }
            if(stack2.empty()){
                while(!stack1.empty()){
                    stack2.push(stack1.pop());
                }
            }
            return stack2.pop();
        }
    }

     

    用两个队列实现栈,要求同上:

    这其实意义不是很大,有些数据结构书上甚至说两个队列不能实现栈。

    其实是可以的,只是时间复杂度较高,一个弹出操作时间为O(N)。

    思路:两个队列,编号为1和2.

    进栈操作:进1号队列

    出栈操作:把1号队列全弄到2号队列里,剩最后一个别压入,而是返回。

    最后还得把1和2号换一下,因为现在是2号有数,1号空。

     

    仅仅有思考价值,不实用。

    比如压入1,2,3

    队列1:1,2,3

    队列2:空

    依次弹出1,2,3:

    队列1里的23进入2号,3弹出

    队列1:空

    队列2:2,3

     

    队列2中3压入1号,2弹出

    队列1:3

    队列2:空

     

    队列1中只有一个元素,弹出。

     

    上代码:

    public class TwoQueueImplStack {
    	Queue<Integer> queue1 = new ArrayDeque<Integer>();
    	Queue<Integer> queue2 = new ArrayDeque<Integer>();
    //压入
    	public void push(Integer element){
    		//都为空,优先1
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    		//1为空,2有数据,放入2
    		if(queue1.isEmpty()){
    			queue2.add(element);
    			return;
    		}
    		//2为空,1有数据,放入1
    		if(queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    	}
    //弹出
    	public Integer pop(){
    		//两个都空,异常
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			try{
    				throw new Exception("satck is empty!");
    			}catch(Exception e){
    				e.printStackTrace();
    			}
    		}	
    		//1空,2有数据,将2中的数据依次放入1,最后一个元素弹出
    		if(queue1.isEmpty()){
    			while(queue2.size() > 1){
    				queue1.add(queue2.poll());
    			}
    			return queue2.poll();
    		}
    		
    		//2空,1有数据,将1中的数据依次放入2,最后一个元素弹出
    		if(queue2.isEmpty()){
    			while(queue1.size() > 1){
    				queue2.add(queue1.poll());
    			}
    			return queue1.poll();
    		}
    		
    		return (Integer)null;
    	}
    //测试
    	public static void main(String[] args) {
    		TwoQueueImplStack qs = new TwoQueueImplStack();
    		qs.push(2);
    		qs.push(4);
    		qs.push(7);
    		qs.push(5);
    		System.out.println(qs.pop());
    		System.out.println(qs.pop());
    		
    		qs.push(1);
    		System.out.println(qs.pop());
    	}
    }
    

     

    第八次笔记 (串)

    串的概念:串(字符串):是由 0 个或多个字符组成的有限序列。 通常记为:s =‘ a1 a2 a3 … ai …an  ’ ( n≥0 )。

    串的逻辑结构和线性表极为相似。

     

    一些串的类型:

     

    空串:不含任何字符的串,长度 = 0。

    空格串:仅由一个或多个空格组成的串。

    子串:由串中任意个连续的字符组成的子序列。

    主串:包含子串的串。

    位置:字符在序列中的序号。

    子串在主串中的位置:子串的首字符在主串中的位置。

     

    空串是任意串的子串,任意串是其自身的子串。

    串相等的条件:当两个串的长度相等且各个对应位置的字符都相等时才相等。

     

    实现:

     

    因为串是特殊的线性表,故其存储结构与线性表的 存储结构类似,只不过组成串的结点是单个字符。

     

    定长顺序存储表示

    也称为静态存储分配的顺序串。 即用一组地址连续的存储单元依次存放串中的字符序列。

     

    串长:可能首字符记录(显式)或\0结尾(隐式)

     

    定长顺序存储表示时串操作的缺点 :串的某些操作受限(截尾),如串的联接、插入、置换

     

    堆分配存储表示  

     

    存储空间在程序执行过程中动态分配,malloc() 分配一块实际串长所需要的存储空间(“堆”)

    堆存储结构的优点:堆存储结构既有顺序存储 结构的特点,处理(随机取子串)方便,操作中对 串长又没有任何限制,更显灵活,因此在串处理的 应用程序中常被采用。

     

    串的块链存储表示

    为了提高空间利用率,可使每个结点存放多个字符 (这是顺序串和链串的综合 (折衷) ),称为块链结构。

     优点:便于插入和删除    缺点:空间利用率低 

     

    串的定长表示

    思想和代码都不难,和线性表也差不多,串本来就是数据受限的线性表。

    串连接:

     

    #include <stdio.h>
    #include <string.h>
    //串的定长顺序存储表示
    #define MAXSTRLEN 255							//用户可在255以内定义最大串长
    typedef unsigned char SString[MAXSTRLEN + 1];	//0号单元存放串的长度
    
    int Concat(SString *T,SString S1,SString S2)
    	//用T返回S1和S2联接而成的新串。若未截断返回1,若截断返回0
    {
    	int i = 1,j,uncut = 0;
    	if(S1[0] + S2[0] <= MAXSTRLEN)	//未截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    		for (j = 1; j <= S2[0]; j++)
    			(*T)[S1[0]+j] = S2[j];	//(*T)[i+j] = S2[j]
    		(*T)[0] = S1[0] + S2[0];
    		uncut = 1;
    	}
    	else if(S1[0] < MAXSTRLEN)		//截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    
    		for (j = S1[0] + 1; j <= MAXSTRLEN; j++)
    		{
    			(*T)[j] = S2[j - S1[0] ];
    			(*T)[0] = MAXSTRLEN;
    			uncut = 0;
    		}
    	}
    	else
    	{
    		for (i = 0; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		/*或者分开赋值,先赋值内容,再赋值长度
    		for (i = 1; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		(*T)[0] = MAXSTRLEN;
    		*/
    		uncut = 0;
    	}
    
    	return uncut;
    }
    
    int SubString(SString *Sub,SString S,int pos,int len)
    	//用Sub返回串S的第pos个字符起长度为len的子串
    	//其中,1 ≤ pos ≤ StrLength(S)且0 ≤ len ≤ StrLength(S) - pos + 1(从pos开始到最后有多少字符)
    	//第1个字符的下标为1,因为第0个字符存放字符长度
    {
    	int i;
    	if(pos < 1 || pos > S[0] || len < 0 || len > S[0] - pos + 1)
    		return 0;
    	for (i = 1; i <= len; i++)
    	{
    		//S中的[pos,len]的元素 -> *Sub中的[1,len]
    		(*Sub)[i] = S[pos + i - 1];//下标运算符 > 寻址运算符的优先级
    	}
    	(*Sub)[0] = len;
    	return 1;
    }
    void PrintStr(SString S)
    {
    	int i;
    	for (i = 1; i <= S[0]; i++)
    		printf("%c",S[i]);
    	printf("\n");
    }
    
    
    int main(void)
    {
    	/*定长顺序存储初始化和打印的方法
    	SString s = {4,'a','b','c','d'};
    	int i;
    	//s = "abc";	//不可直接赋值
    	for (i = 1; i <= s[0]; i++)
    		printf("%c",s[i]);
    	*/
    	SString s1 = {4,'a','b','c','d'};
    	SString s2 = {4,'e','f','g','h'},s3;
    	SString T,Sub;
    	int i;
    	
    	for (i = 1; i <= 255; i++)
    	{
    		s3[i] = 'a';
    		if(i >= 248)
    			s3[i] = 'K';
    	}
    	s3[0] = 255;
    	SubString(&Sub,s3,247,8);
    	PrintStr(Sub);
    	
    
    
    
    	return 0;
    }

    第九次笔记(数组,广义表)

    数组:按一定格式排列起来的具有相同类型的数据元素的集合。

     

    二维数组:若一维数组中的数据元素又是一维数组结构,则称为二维数组。 

    同理,推广到多维数组。若 n -1 维数组中的元素又是一个一维数组结构,则称作 n 维数组。 

    声明格式:数据类型   变量名称[行数] [列数] ;

     

    实现:一般都是采用顺序存储结构来表示数组。

     

    二维数组两种顺序存储方式:以行序为主序 (低下标优先) 、以列序为主序 (高下标优先)

    一个二维数组 A,行下标的范围是 1 到 6,列下标的范围是 0 到 7,每个数组元素用相邻的6 个字节存储,存储器按字节编址。那么,这个数组的体积是288个字节。

     

     广义表(又称列表 Lists)是n≥0个元素 a1, a2, …, an 的有限序列,其中每一个ai 或者是原子,或者是一个子表。

     

    表头:若 LS 非空 (n≥1 ),则其第一个元素 a1 就是表头。

     表尾:除表头之外的其它元素组成的表。记作  tail(LS) = (a2, ..., an)。 

     

    (( )) 长度为 1,表头、表尾均为 ( )

    (a, (b, c))长度为 2,由原子 a 和子表 (b, c) 构成。表头为 a ;表尾为 ((b, c))。

     

    广义表的长度定义为最外层所包含元素的个数

    广义表的深度定义为该广义表展开后所含括号的重数。

    “原子”的深度为 0 ;  “空表”的深度为 1 。

     

    取表头运算 GetHead  和取表尾运算 GetTail

    GetHead(LS) = a1        GetTail(LS) = (a2, …, an)。

     

    广义表可看成是线性表的推广,线性表是广义表的特例。

     

    广义表的结构相当灵活,在某种前提下,它可以兼容线 性表、数组、树和有向图等各种常用的数据结构。

    由于广义表不仅集中了线性表、数组、树和有向图等常 见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。 

     

     

    第十次笔记(树和二叉树)

     

    树的定义:树(Tree)是 n(n≥0)个结点的有限集。若 n=0,称为空树;若 n > 0,则它满足如下两个条件:  

    (1)  有且仅有一个特定的称为根 (Root) 的结点;  

    (2)  其余结点可分为 m (m≥0) 个互不相交的有限集 T1, T2, T3, …, Tm, 其中每一个集合本身又是一棵树,并称为根的子树 (SubTree)。

    显然,树的定义是一个递归的定义。

    树的一些术语:

    • 结点的度(Degree):结点的子树个数;
    • 树的度:树的所有结点中最大的度数;
    • 叶结点(Leaf):度为0的结点;
    • 父结点(Parent):有子树的结点是其子树的根节点的父结点;
    • 子结点/孩子结点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点;
    • 兄弟结点(Sibling):具有同一个父结点的各结点彼此是兄弟结点;
    • 路径和路径长度:从结点n1到nk的路径为一个结点序列n1,n2,...,nk。ni是ni+1的父结点。路径所包含边的个数为路径的长度;
    • 祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点;
    • 子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙;
    • 结点的层次(Level):规定根结点在1层,其他任一结点的层数是其父结点的层数加1;
    • 树的深度(Depth):树中所有结点中的最大层次是这棵树的深度;

    将树中节点的各子树看成从左至右是有次序的(即不能互换),则称为该树是有序树,否则称为无序树

    森林(forest)是 m (m≥0) 棵互不相交的树的集合。

     

    二叉树

     

    在计算机科学中,二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。

     

    虽然二叉树与树概念不同,但有关树的基本术语对二叉树都适用。

     

    二叉树结点的子树要区分左子树和右子树,即使只有一 棵子树也要进行区分,说明它是左子树,还是右子树。树当 结点只有一个孩子时,就无须区分它是左还是右。

     

    注意:尽管二叉树与树有许多相似之处,但二叉树不是树的特殊情形。

    一些性质:

    在二叉树的第 i 层上至多有  个结点 (i ≥1)。

    证明:每个节点至多两个孩子,每一层至多比上一层多一倍的结点,根为1.

     

    深度为 k 的二叉树至多有 个结点(k ≥1)。

    证明:把每一层最大节点加起来即可

     

    对任何一棵二叉树 T,如果其叶子数为 n0,度为 2的结点数为 n2,则 n0 = n2 + 1。

    证明:对于一个只有根的树,n0 = n2 + 1成立。1=0+1

    我们把一个叶子节点换成度为2的结点:

    黑色节点原来为叶子节点

    我们发现,度为2的结点数加1(黑色节点);叶子节点数加1(原来的去掉,新增两个);对于式子n0 = n2 + 1没影响,还是成立。

     

    我们把叶子节点换成度为1的结点,比如没有右孩子。

    我们发现,度为2的结点数没变。叶子节点数没变(减了一个加了一个)

    所以,不管你怎么换,公式都成立。(佛系证明)

     

     

    二叉树概述

     

    各种实现和应用以后放链接

    一、二叉树的基本概念

    二叉树:二叉树是每个节点最多有两个子树的树结构。

    根节点:一棵树最上面的节点称为根节点。

    父节点子节点:如果一个节点下面连接多个节点,那么该节点称为父节点,它下面的节点称为子 节点。

    叶子节点:没有任何子节点的节点称为叶子节点。

    兄弟节点:具有相同父节点的节点互称为兄弟节点。

    节点度:节点拥有的子树数。上图中,13的度为2,46的度为1,28的度为0。

    树的深度:从根节点开始(其深度为0)自顶向下逐层累加的。上图中,13的深度是1,30的深度是2,28的深度是3。

    树的高度:从叶子节点开始(其高度为0)自底向上逐层累加的。54的高度是2,根节点23的高度是3。

    对于树中相同深度的每个节点来说,它们的高度不一定相同,这取决于每个节点下面的叶子节点的深度。上图中,13和54的深度都是1,但是13的高度是1,54的高度是2。

    二、二叉树的类型

    类型 定义 图示

    满二叉树

    Full Binary Tree

    除最后一层无任何子节点外,每一层上的所有节点都有两个子节点,最后一层都是叶子节点。满足下列性质:

    1)一颗树深度为h,最大层数为k,深度与最大层数相同,k=h;

    2)叶子节点数(最后一层)为2k−1;

    3)第 i 层的节点数是:2i−1;

    4)总节点数是:2k−1,且总节点数一定是奇数。

    完全二叉树

    Complete Binary Tree

    若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。满足下列性质:

    1)只允许最后一层有空缺结点且空缺在右边,即叶子节点只能在层次最大的两层上出现;

    2)对任一节点,如果其右子树的深度为j,则其左子树的深度必为j或j+1。 即度为1的点只有1个或0个;

    3)除最后一层,第 i 层的节点数是:2i−1;

    4)有n个节点的完全二叉树,其深度为:log2n+1或为log2n+1;

    5)满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

    平衡二叉树

    Balanced Binary Tree

    又被称为AVL树,它是一颗空树或左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。

    二叉搜索树

    Binary Search Tree

    又称二叉查找树、二叉排序树(Binary Sort Tree)。它是一颗空树或是满足下列性质的二叉树:

    1)若左子树不空,则左子树上所有节点的值均小于或等于它的根节点的值;

    2)若右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;

    3)左、右子树也分别为二叉排序树。

    红黑树

    Red Black Tree

    是每个节点都带有颜色属性(颜色为红色或黑色)的自平衡二叉查找树,满足下列性质:

    1)节点是红色或黑色;

    2)根节点是黑色;

    3)所有叶子节点都是黑色;

    4)每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)

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

     

     

     

     

     

     

     

     

     

    啦啦啦

     

     

    第十一次笔记(满二叉树,完全二叉树)

    因为图片丢失,内容不全,我尽量找一下图

    满二叉树 (Full binary tree)

    除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树。

    国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。

    国外(国际)定义:a binary tree T is full if each node is either a leaf or possesses exactly two childnodes.

    大意为:如果一棵二叉树的结点要么是叶子结点,要么它有两个子结点,这样的树就是满二叉树。(一棵满二叉树的每一个结点要么是叶子结点,要么它有两个子结点,但是反过来不成立,因为完全二叉树也满足这个要求,但不是满二叉树)

    从图形形态上看,满二叉树外观上是一个三角形。

    这里缺失公式

    完全二叉树

     

    完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。

    可以根据公式进行推导,假设n0是度为0的结点总数(即叶子结点数),n1是度为1的结点总数,n2是度为2的结点总数,则 :

    ①n= n0+n1+n2 (其中n为完全二叉树的结点总数);又因为一个度为2的结点会有2个子结点,一个度为1的结点会有1个子结点,除根结点外其他结点都有父结点,

    ②n= 1+n1+2*n2 ;由①、②两式把n2消去得:n= 2*n0+n1-1,由于完全二叉树中度为1的结点数只有两种可能0或1,由此得到n0=n/2 或 n0=(n+1)/2。

    简便来算,就是 n0=n/2,其中n为奇数时(n1=0)向上取整;n为偶数时(n1=1)。可根据完全二叉树的结点总数计算出叶子结点数。

     

    重点:出于简便起见,完全二叉树通常采用数组而不是链表存储

     

    对于tree[i]有如下特点:

    (1)若i为奇数且i>1,那么tree的左兄弟为tree[i-1];

    (2)若i为偶数且i<n,那么tree的右兄弟为tree[i+1];

    (3)若i>1,tree的父亲节点为tree[i div 2];

    (4)若2*i<=n,那么tree的左孩子为tree[2*i];若2*i+1<=n,那么tree的右孩子为tree[2*i+1];

    (5)若i>n div 2,那么tree[i]为叶子结点(对应于(3));

    (6)若i<(n-1) div 2.那么tree[i]必有两个孩子(对应于(4))。

    (7)满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

    完全二叉树第i层至多有2^(i-1)个节点,共i层的完全二叉树最多有2^i-1个节点。

    特点:

    1)只允许最后一层有空缺结点且空缺在右边,即叶子结点只能在层次最大的两层上出现;

    2)对任一结点,如果其右子树的深度为j,则其左子树的深度必为j或j+1。 即度为1的点只有1个或0个

     

    第十二次笔记(二叉树的存储和遍历)

     

    顺序存储结构

     

    完全二叉树:用一组地址连续的 存储单元依次自上而下、自左至右存 储结点元素,即将编号为 i  的结点元 素存储在一维数组中下标为 i –1 的分量中。

    一般二叉树:将其每个结点与完 全二叉树上的结点相对照,存储在一 维数组的相应分量中。

     

    最坏情况:树退化为线性后:

    我们要把它“变”成这个大家伙来存了:

    深度为 k 的且只 有 k 个结点的右单支树需要 长度为2^k-1 的一维数组。 

     

    链式存储结构

    lchild和rchild都是指向相同结构的指针

    在 n 个结点的二叉链表中有 n + 1 个空指针域。

    typedef struct BiTNode { // 结点结构
        TElemType data;
        struct BiTNode *lchild,*rchild;// 左右孩子指针
    } BiTNode, *BiTree;
    

    可以多一条指向父的指针。

     

    遍历二叉树

     

    顺着某一条搜索路径巡访二叉树中的结点,使   得每个结点均被访问一次,而且仅被访问一次

     “访问”的含义很广,可以是对结点作各种处理, 如:输出结点的信息、修改结点的数据值等,但要求这种访问不破坏原来的数据结构。

    (所以有些题目比如morris遍历、链表后半段反转判断回文等等必须进行完,解题时就算已经得出答案也要遍历完,因为我们不能改变原来的数据结构。)

     

    具体遍历的介绍

    https://blog.csdn.net/hebtu666/article/details/82853988

     

    深入理解二叉树遍历

    二叉树:二叉树是每个节点最多有两个子树的树结构。

     

    本文介绍二叉树的遍历相关知识。

    我们学过的基本遍历方法,无非那么几个:前序,中序,后序,还有按层遍历等等。

    设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

    首先我们定义一颗二叉树

    typedef char ElementType;
    typedef struct TNode *Position;
    typedef Position BinTree;
    struct TNode{
        ElementType Data;
        BinTree Left;
        BinTree Right;
    };
    

    前序

    首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树

    思路:

    就是利用函数,先打印本个节点,然后对左右子树重复此过程即可。

    void PreorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        printf(" %c", BT->Data);
        PreorderTraversal(BT->Left);
        PreorderTraversal(BT->Right);
    }

     

    中序

    首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树

    思路:

    还是利用函数,先对左边重复此过程,然后打印根,然后对右子树重复。

    void InorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        InorderTraversal(BT->Left);
        printf(" %c", BT->Data);
        InorderTraversal(BT->Right);
    }

    后序

    首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根

    思路:

    先分别对左右子树重复此过程,然后打印根

    void PostorderTraversal(BinTree BT)
    {
        if(BT==NULL)return ;
        PostorderTraversal(BT->Left);
        PostorderTraversal(BT->Right);
        printf(" %c", BT->Data);
    }

    进一步思考

    看似好像很容易地写出了三种遍历。。。。。

     

    但是你真的理解为什么这么写吗?

    比如前序遍历,我们真的是按照定义里所讲的,首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树。这种过程来遍历了一遍二叉树吗?

    仔细想想,其实有一丝不对劲的。。。

    再看代码:

    void Traversal(BinTree BT)//遍历
    {
    //1111111111111
        Traversal(BT->Left);
    //22222222222222
        Traversal(BT->Right);
    //33333333333333
    }

    为了叙述清楚,我给三个位置编了号 1,2,3

    我们凭什么能前序遍历,或者中序遍历,后序遍历?

    我们看,前序中序后序遍历,实现的代码其实是类似的,都是上面这种格式,只是我们分别在位置1,2,3打印出了当前节点而已啊。我们凭什么认为,在1打印,就是前序,在2打印,就是中序,在3打印,就是后序呢?不管在位置1,2,3哪里操作,做什么操作,我们利用函数遍历树的顺序变过吗?当然没有啊。。。

    都是三次返回到当前节点的过程:先到本个节点,也就是位置1,然后调用了其他函数,最后调用完了,我们开到了位置2。然后又调用别的函数,调用完了,我们来到了位置3.。然后,最后操作完了,这个函数才结束。代码里的三个位置,每个节点都被访问了三次。

    而且不管位置1,2,3打印了没有,操作了没有,这个顺序是永远存在的,不会因为你在位置1打印了,顺序就改为前序,你在位置2打印了,顺序就成了中序。

     

    为了有更直观的印象,我们做个试验:在位置1,2,3全都放入打印操作;

    我们会发现,每个节点都被打印了三次。而把每个数第一次出现拿出来,就组成了前序遍历的序列;所有数字第二次出现拿出来,就组成了中序遍历的序列。。。。

     

    其实,遍历是利用了一种数据结构:栈

    而我们这种写法,只是通过函数,来让系统帮我们压了栈而已。为什么能实现遍历?为什么我们访问完了左子树,能返回到当前节点?这都是栈的功劳啊。我们把当前节点(对于函数就是当时的现场信息)存到了栈里,记录下来,后来才能把它拿了出来,能回到以前的节点。

     

    想到这里,可能就有更深刻的理解了。

    我们能否不用函数,不用系统帮我们压栈,而是自己做一个栈,来实现遍历呢?

    先序实现思路:拿到一个节点的指针,先判断是否为空,不为空就先访问(打印)该结点,然后直接进栈,接着遍历左子树;为空则要从栈中弹出一个节点来,这个时候弹出的结点就是其父亲,然后访问其父亲的右子树,直到当前节点为空且栈为空时,结束。

    核心思路代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            //visit(p);进行操作
            st.push(p);//入栈
            p = p->lchild;左
        } 
        else//空
        {
            p = st.top();//取出
            st.pop();
            p = p->rchild;//右
        }
    }

    中序实现思路:和前序遍历一样,只不过在访问节点的时候顺序不一样,访问节点的时机是从栈中弹出元素时访问,如果从栈中弹出元素,就意味着当前节点父亲的左子树已经遍历完成,这时候访问父亲,就是中序遍历.

    (对应递归是第二次遇到)

    核心代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            st.push(p);//压入
            p = p->lchild;
        }
        else//空
        {
            p = st.top();//取出
            //visit(p);操作
            st.pop();
            p = p->rchild;
        }
    }

    后序遍历是最难的。因为要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难点。

    因为我们原来说了,后序是第三次遇到才进行操作的,所以我们很容易有这种和递归函数类似的思路:对于任一结点,将其入栈,然后沿其左子树一直往下走,一直走到没有左孩子的结点,此时该结点在栈顶,但是不能出栈访问, 因此右孩子还没访问。所以接下来按照相同的规则对其右子树进行相同的处理。访问完右孩子,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。

    第二种思路:对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,或者左孩子和右孩子都已被访问过了,就可以直接访问该结点。如果有孩子未访问,将P的右孩子和左孩子依次入栈。

    网上的思路大多是第一种,所以我在这里给出第二种的大概实现吧

    首先初始化cur,pre两个指针,代表访问的当前节点和之前访问的节点。把根放入,开始执行。

    s.push(root);
    while(!s.empty())
    {
        cur=s.top();
        if((cur->lchild==NULL && cur->rchild==NULL)||(pre!=NULL && (pre==cur->lchild||pre==cur->rchild)))
        {
            //visit(cur);  如果当前结点没有孩子结点或者孩子节点都已被访问过 
            s.pop();//弹出
            pre=cur; //记录
        }
        else//分别放入右左孩子
        {
            if(cur->rchild!=NULL)
                s.push(cur->rchild);
            if(cur->lchild!=NULL)    
                s.push(cur->lchild);
        }
    }

    这两种方法,都是利用栈结构来实现的遍历,需要一定的栈空间,而其实存在一种时间O(N),空间O(1)的遍历方式,下次写了我再放链接。

     

    斗个小机灵:后序是LRD,我们其实已经知道先序是DLR,那其实我们可以用先序来实现后序啊,我们只要先序的时候把左右子树换一下:DRL(这一步很好做到),然后倒过来不就是DRL了嘛。。。。。就把先序代码改的左右反过来,然后放栈里倒过来就好了,不需要上面介绍的那些复杂的方法。。。。

    第十四次笔记(树的存储)

     

     

    父节点表示法

     

    数据域:存放结点本身信息。

    双亲域:指示本结点的双亲结点在数组中的位置。

    对应的树:

    /* 树节点的定义 */
    #define MAX_TREE_SIZE 100
     
    typedef struct{
        TElemType data;
        int parent; /* 父节点位置域 */
    } PTNode;
     
    typedef struct{
        PTNode nodes[MAX_TREE_SIZE];
        int n; /* 节点数 */
    } PTree;

    特点:找双亲容易,找孩子难。

    孩子表示法(树的链式存储结构)

     

    childi指向一个结点

    可以加上parent。

    在有 n 个结点、度为  d 的树的 d 叉链表中,有  n×(d-1)+1 个空链域

     

    我们可以用degree记录有几个孩子,省掉空间,但是结点的指针个数不相等,为该结点的度 degree。

     

    孩子链表:

     

    把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则 n 个结点有 n 个孩子链表(叶子的孩子链表为空表)。而 n 个头指针又组成一个线性表,用顺序表(含 n 个元素的结构数组)存储。

    孩子兄弟表示法(二叉树表示法)

    用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点

    typedef struct CSNode{
         ElemType data;
         struct CSNode *firstchild, *nextsibling;  
    } CSNode, *CSTree;
    

    第十五次笔记(图基础)

     

    图是一种:   数据元素间存在多对多关系的数据结构   加上一组基本操作构成的抽象数据类型。

    图 (Graph) 是一种复杂的非线性数据结构,由顶点集合及顶点间的关系(也称弧或边)集合组成。可以表示为: G=(V, VR)  

    其中 V 是顶点的有穷非空集合;

    VR 是顶点之间   关系的有穷集合,也叫做弧或边集合。

    弧是顶点的有序对,边是顶点的无序对。

     

    特点:(相对于线性结构)

    顶点之间的关系是任意的 

    图中任意两个顶点之间都可能相关

    顶点的前驱和后继个数无限制

     

    相关概念:

     

    顶点(Vertex):图中的数据元素。线性表中我们把数据元素叫元素,树中将数据元素叫结点。

    边:顶点之间的逻辑关系用边来表示,边集可以是空的。

     

    无向边(Edge):若顶点V1到V2之间的边没有方向,则称这条边为无向边。

    无向图(Undirected graphs):图中任意两个顶点之间的边都是无向边。(A,D)=(D,A)

    无向图中边的取值范围:0≤e≤n(n-1)/2

    有向边:若从顶点V1到V2的边有方向,则称这条边为有向边,也称弧(Arc)。用<V1,V2>表示,V1为狐尾(Tail),V2为弧头(Head)。(V1,V2)≠(V2,V1)。

    有向图(Directed graphs):图中任意两个顶点之间的边都是有向边。

    有向图中弧的取值范围:0≤e≤n(n-1)

       注意:无向边用“()”,而有向边用“< >”表示。

     

    简单图:图中不存在顶点到其自身的边,且同一条边不重复出现。

    无向完全图:无向图中,任意两个顶点之间都存在边。

    有向完全图:有向图中,任意两个顶点之间都存在方向互为相反的两条弧。

    稀疏图:有很少条边。

    稠密图:有很多条边。

     

    邻接点:若 (v, v´) 是一条边,则称顶点 v 和 v´互为 邻接点,或称 v 和 v´相邻接;称边 (v, v´) 依附于顶点 v 和 v´,或称 (v, v´) 与顶点 v 和 v´ 相关联。

     

    权(Weight):与图的边或弧相关的数。

    网(Network):带权的图。

    子图(Subgraph):假设G=(V,{E})和G‘=(V',{E'}),如果V'包含于V且E'包含于E,则称G'为G的子图。

     

     入度:有向图中以顶点 v 为头的弧的数目称为 v 的入度,记为:ID(v)。  

    出度:有向图中以顶点 v 为尾的弧的数目称为 v 的出度,记为:OD(v)。

    度(Degree):无向图中,与顶点V相关联的边的数目。有向图中,入度表示指向自己的边的数目,出度表示指向其他边的数目,该顶点的度等于入度与出度的和。

     

    回路(环):第一个顶点和最后一个顶点相同的路径。

    简单路径:序列中顶点(两端点除外)不重复出现的路径。 

    简单回路(简单环):前后两端点相同的简单路径。

    路径的长度:一条路径上边或弧的数量。

     

    连通:从顶点 v 到 v´ 有路径,则说 v  和 v´ 是连通的。

    连通图:图中任意两个顶点都是连通的。

    连通分量:无向图的极大连通子图(不存在包含它的 更大的连通子图);

    任何连通图的连通分量只有一个,即其本身;非连通图有多个连通分量(非连通图的每一个连通部分)。

    强连通图: 任意两个顶点都连通的有向图。 

    强连通分量:有向图的极大强连通子图;任何强连通 图的强连通分量只有一个,即其本身;非强连通图有多个 强连通分量。

     

    生成树:所有顶点均由边连接在一起但不存在回路的图。(n个顶点n-1条边)

     

     

     

     

    图的存储

     

    多重链表:完全模拟图的样子,每个节点内的指针都指向该指向的节点。

    节点结构内指针数为度

    缺点:浪费空间、不容易操作

     

    数组表示法(邻接矩阵表示法)

    可用两个数组存储。其中一个 一维数组存储数据元素(顶点)的信息,另一个二维数组 (邻接矩阵)存储数据元素之间的关系(边或弧)的信息

    有向图:

    有向网:

    缺点:用于稀疏图时空间浪费严重

    优点:操作较容易

     

    邻接表

    指针数组存放每个结点,每个结点后接所有能到达的节点。

     

    图的遍历

     

    从图的任意指定顶点出发,依照某种规则去访问图中所有顶 点,且每个顶点仅被访问一次,这一过程叫做图的遍历。

    图的遍历按照深度优先和广度优先规则去实施,通常有深度 优先遍历法(Depth_First Search——DFS )和  广度优先遍历法 ( Breadth_Frist Search——BFS)两种。

    简单棋盘搜索https://blog.csdn.net/hebtu666/article/details/81483407

    别的实现以后再贴

    如何判别V的邻接点是否被访问?

    为每个顶点设立一个“访问标志”。

     

    最小生成树

    问题提出:
        要在n个城市间建立通信联络网。顶点:表示城市,权:城市间通信线路的花费代价。希望此通信网花费代价最小。
    问题分析:
        答案只能从生成树中找,因为要做到任何两个城市之间有线路可达,通信网必须是连通的;但对长度最小的要求可以知道网中显然不能有圈,如果有圈,去掉一条边后,并不破坏连通性,但总代价显然减少了,这与总代价最小的假设是矛盾的。
    结论:
        希望找到一棵生成树,它的每条边上的权值之和(即建立该通信网所需花费的总代价)最小 —— 最小代价生成树。
        构造最小生成树的算法很多,其中多数算法都利用了一种称之为 MST 的性质。
        MST 性质:设 N = (V, E)  是一个连通网,U是顶点集 V的一个非空子集。若边 (u, v) 是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边 (u, v) 的最小生成树。


    (1)普里姆 (Prim) 算法

    算法思想: 
        ①设 N=(V, E)是连通网,TE是N上最小生成树中边的集合。
        ②初始令 U={u_0}, (u_0∈V), TE={ }。
        ③在所有u∈U,u∈U-V的边(u,v)∈E中,找一条代价最小的边(u_0,v_0 )。
        ④将(u_0,v_0 )并入集合TE,同时v_0并入U。
        ⑤重复上述操作直至U = V为止,则 T=(V,TE)为N的最小生成树。

     
    代码实现:

    void MiniSpanTree_PRIM(MGraph G,VertexType u)
        //用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。
        //记录从顶点集U到V-U的代价最小的边的辅助数组定义;
        //closedge[j].lowcost表示在集合U中顶点与第j个顶点对应最小权值
    {
        int k, j, i;
        k = LocateVex(G,u);
        for (j = 0; j < G.vexnum; ++j)    //辅助数组的初始化
            if(j != k)
            {
                closedge[j].adjvex = u;
                closedge[j].lowcost = G.arcs[k][j].adj;    
    //获取邻接矩阵第k行所有元素赋给closedge[j!= k].lowcost
            }
        closedge[k].lowcost = 0;        
    //初始,U = {u};  
        PrintClosedge(closedge,G.vexnum);
        for (i = 1; i < G.vexnum; ++i)    \
    //选择其余G.vexnum-1个顶点,因此i从1开始循环
        {
            k = minimum(G.vexnum,closedge);        
    //求出最小生成树的下一个结点:第k顶点
            PrintMiniTree_PRIM(G, closedge, k);     //输出生成树的边
            closedge[k].lowcost = 0;                //第k顶点并入U集
            PrintClosedge(closedge,G.vexnum);
            for(j = 0;j < G.vexnum; ++j)
            {                                           
                if(G.arcs[k][j].adj < closedge[j].lowcost)    
    //比较第k个顶点和第j个顶点权值是否小于closedge[j].lowcost
                {
                    closedge[j].adjvex = G.vexs[k];//替换closedge[j]
                    closedge[j].lowcost = G.arcs[k][j].adj;
                    PrintClosedge(closedge,G.vexnum);
                }
            }
        }
    }


    (2)克鲁斯卡尔 (Kruskal) 算法

    算法思想: 
        ①设连通网  N = (V, E ),令最小生成树初始状态为只有n个顶点而无边的非连通图,T=(V, { }),每个顶点自成一个连通分量。
        ②在 E 中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
    ③依此类推,直至 T 中所有顶点都在同一连通分量上为止。
          
        最小生成树可能不惟一!

     

    拓扑排序

    (1)有向无环图

        无环的有向图,简称 DAG (Directed Acycline Graph) 图。
     
    有向无环图在工程计划和管理方面的应用:除最简单的情况之外,几乎所有的工程都可分为若干个称作“活动”的子工程,并且这些子工程之间通常受着一定条件的约束,例如:其中某些子工程必须在另一些子工程完成之后才能开始。
    对整个工程和系统,人们关心的是两方面的问题: 
    ①工程能否顺利进行; 
    ②完成整个工程所必须的最短时间。

    对应到有向图即为进行拓扑排序和求关键路径。 
    AOV网: 
        用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network)。
    例如:排课表
          
    AOV网的特点:
    ①若从i到j有一条有向路径,则i是j的前驱;j是i的后继。
    ②若< i , j >是网中有向边,则i是j的直接前驱;j是i的直接后继。
    ③AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。


    问题:    
        问题:如何判别 AOV 网中是否存在回路?
        检测 AOV 网中是否存在环方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。


    拓扑排序的方法:
        ①在有向图中选一个没有前驱的顶点且输出之。
        ②从图中删除该顶点和所有以它为尾的弧。
        ③重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止。
            
        一个AOV网的拓扑序列不是唯一的!
    代码实现:

    Status TopologicalSort(ALGraph G)
        //有向图G采用邻接表存储结构。
        //若G无回路,则输出G的顶点的一个拓扑序列并返回OK,否则返回ERROR.
        //输出次序按照栈的后进先出原则,删除顶点,输出遍历
    {
        SqStack S;
        int i, count;
        int *indegree1 = (int *)malloc(sizeof(int) * G.vexnum);
        int indegree[12] = {0};
        FindInDegree(G, indegree);    //求个顶点的入度下标从0开始
        InitStack(&S);
        PrintStack(S);
        for(i = 0; i < G.vexnum; ++i)
            if(!indegree[i])        //建0入度顶点栈S
                push(&S,i);        //入度为0者进栈
        count = 0;                //对输出顶点计数
        while (S.base != S.top)
        {
            ArcNode* p;
            pop(&S,&i);
            VisitFunc(G,i);//第i个输出栈顶元素对应的顶点,也就是最后进来的顶点    
            ++count;          //输出i号顶点并计数
            for(p = G.vertices[i].firstarc; p; p = p->nextarc)
            {    //通过循环遍历第i个顶点的表结点,将表结点中入度都减1
                int k = p->adjvex;    //对i号顶点的每个邻接点的入度减1
                if(!(--indegree[k]))
                    push(&S,k);        //若入度减为0,则入栈
            }//for
        }//while
        if(count < G.vexnum)
        {
            printf("\n该有向图有回路!\n");
            return ERROR;    //该有向图有回路
        }
        else
        {
            printf("\n该有向图没有回路!\n");
            return OK;
        }
    }


    关键路径

        把工程计划表示为有向图,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。称这种有向图为边表示活动的网,简称为 AOE网 (Activity On Edge)。
    例如:
    设一个工程有11项活动,9个事件。
    事件v_1——表示整个工程开始(源点) 
    事件v_9——表示整个工程结束(汇点)

     
    对AOE网,我们关心两个问题:  
    ①完成整项工程至少需要多少时间? 
    ②哪些活动是影响工程进度的关键?
    关键路径——路径长度最长的路径。
    路径长度——路径上各活动持续时间之和。
    v_i——表示事件v_i的最早发生时间。假设开始点是v_1,从v_1到〖v�i〗的最长路径长度。ⅇ(ⅈ)——表示活动a_i的最早发生时间。
    l(ⅈ)——表示活动a_i最迟发生时间。在不推迟整个工程完成的前提下,活动a_i最迟必须开始进行的时间。
    l(ⅈ)-ⅇ(ⅈ)意味着完成活动a_i的时间余量。
    我们把l(ⅈ)=ⅇ(ⅈ)的活动叫做关键活动。显然,关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程进度。
        例如上图中网,从从v_1到v_9的最长路径是(v_1,v_2,v_5,v_8,ν_9 ),路径长度是18,即ν_9的最迟发生时间是18。而活动a_6的最早开始时间是5,最迟开始时间是8,这意味着:如果a_6推迟3天或者延迟3天完成,都不会影响整个工程的完成。因此,分析关键路径的目的是辨别哪些是关键活动,以便争取提高关键活动的工效,缩短整个工期。
        由上面介绍可知:辨别关键活动是要找l(ⅈ)=ⅇ(ⅈ)的活动。为了求ⅇ(ⅈ)和l(ⅈ),首先应求得事件的最早发生时间vⅇ(j)和最迟发生时间vl(j)。如果活动a_i由弧〈j,k〉表示,其持续时间记为dut(〈j,k〉),则有如下关系:
    ⅇ(ⅈ)= vⅇ(j)
    l(ⅈ)=vl(k)-dut(〈j,k〉)
        求vⅇ(j)和vl(j)需分两步进行:
    第一步:从vⅇ(0)=0开始向前递推
    vⅇ(j)=Max{vⅇ(i)+dut(〈j,k〉)}   〈i,j〉∈T,j=1,2,…,n-1
    其中,T是所有以第j个顶点为头的弧的集合。
    第二步:从vl(n-1)=vⅇ(n-1)起向后递推
    vl(i)=Min{vl(j)-dut(〈i,j〉)}  〈i,j〉∈S,i=n-2,…,0
    其中,S是所有以第i个顶点为尾的弧的集合。
    下面我们以上图AOE网为例,先求每个事件v_i的最早发生时间,再逆向求每个事件对应的最晚发生时间。再求每个活动的最早发生时间和最晚发生时间,如下面表格:
              
    在活动的统计表中,活动的最早发生时间和最晚发生时间相等的,就是关键活动


    关键路径的讨论:

    ①若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。      如:a11、a10、a8、a7。 
    ②如果一个活动处于所有的关键路径上,则提高这个活动的速度,就能缩短整个工程的完成时间。如:a1、a4。
    ③处于所有关键路径上的活动完成时间不能缩短太多,否则会使原关键路径变成非关键路径。这时必须重新寻找关键路径。如:a1由6天变成3天,就会改变关键路径。

    关键路径算法实现:

    int CriticalPath(ALGraph G)
    {    //因为G是有向网,输出G的各项关键活动
        SqStack T;
        int i, j;    ArcNode* p;
        int k , dut;
        if(!TopologicalOrder(G,T))
            return 0;
        int vl[VexNum];
        for (i = 0; i < VexNum; i++)
            vl[i] = ve[VexNum - 1];        //初始化顶点事件的最迟发生时间
        while (T.base != T.top)            //按拓扑逆序求各顶点的vl值
        {
     
            for(pop(&T, &j), p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                k = p->adjvex;    dut = *(p->info);    //dut<j, k>
                if(vl[k] - dut < vl[j])
                    vl[j] = vl[k] - dut;
            }//for
        }//while
        for(j = 0; j < G.vexnum; ++j)    //求ee,el和关键活动
        {
            for (p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                int ee, el;        char tag;
                k = p->adjvex;    dut = *(p->info);
                ee = ve[j];    el = vl[k] - dut;
                tag = (ee == el) ? '*' : ' ';
                PrintCriticalActivity(G,j,k,dut,ee,el,tag);
            }
        }
        return 1;
    }

     

    最短路 

    最短路

        典型用途:交通网络的问题——从甲地到乙地之间是否有公路连通?在有多条通路的情况下,哪一条路最短?
     
        交通网络用有向网来表示:顶点——表示城市,弧——表示两个城市有路连通,弧上的权值——表示两城市之间的距离、交通费或途中所花费的时间等。
        如何能够使一个城市到另一个城市的运输时间最短或运费最省?这就是一个求两座城市间的最短路径问题。
        问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n - 1条边。
       常见最短路径问题:单源点最短路径、所有顶点间的最短路径
    (1)如何求得单源点最短路径?
        穷举法:将源点到终点的所有路径都列出来,然后在其中选最短的一条。但是,当路径特别多时,特别麻烦;没有规律可循。
        迪杰斯特拉(Dijkstra)算法:按路径长度递增次序产生各顶点的最短路径。
    路径长度最短的最短路径的特点:
        在此路径上,必定只含一条弧 <v_0, v_1>,且其权值最小。由此,只要在所有从源点出发的弧中查找权值最小者。
    下一条路径长度次短的最短路径的特点:
    ①、直接从源点到v_2<v_0, v_2>(只含一条弧);
    ②、从源点经过顶点v_1,再到达v_2<v_0, v_1>,<v_1, v_2>(由两条弧组成)
    再下一条路径长度次短的最短路径的特点:
        有以下四种情况:
        ①、直接从源点到v_3<v_0, v_3>(由一条弧组成);
        ②、从源点经过顶点v_1,再到达v_3<v_0, v_1>,<v_1, v_3>(由两条弧组成);
        ③、从源点经过顶点v_2,再到达v_3<v_0, v_2>,<v_2, v_3>(由两条弧组成);
        ④、从源点经过顶点v_1  ,v_2,再到达v_3<v_0, v_1>,<v_1, v_2>,<v_2, v_3>(由三条弧组成);
    其余最短路径的特点:    
        ①、直接从源点到v_i<v_0, v_i>(只含一条弧);
        ②、从源点经过已求得的最短路径上的顶点,再到达v_i(含有多条弧)。
    Dijkstra算法步骤:
        初始时令S={v_0},  T={其余顶点}。T中顶点对应的距离值用辅助数组D存放。
        D[i]初值:若<v_0, v_i>存在,则为其权值;否则为∞。 
        从T中选取一个其距离值最小的顶点v_j,加入S。对T中顶点的距离值进行修改:若加进v_j作中间顶点,从v_0到v_i的距离值比不加 vj 的路径要短,则修改此距离值。
        重复上述步骤,直到 S = V 为止。

    算法实现:

    void ShortestPath_DIJ(MGraph G,int v0,PathMatrix &P,ShortPathTable &D)
    { // 用Dijkstra算法求有向网 G 的 v0 顶点到其余顶点v的最短路径P[v]及带权长度D[v]。
        // 若P[v][w]为TRUE,则 w 是从 v0 到 v 当前求得最短路径上的顶点。  P是存放最短路径的矩阵,经过顶点变成TRUE
        // final[v]为TRUE当且仅当 v∈S,即已经求得从v0到v的最短路径。
        int v,w,i,j,min;
        Status final[MAX_VERTEX_NUM];
        for(v = 0 ;v < G.vexnum ;++v)
        {
            final[v] = FALSE;
            D[v] = G.arcs[v0][v].adj;        //将顶点数组中下标对应是 v0 和 v的距离给了D[v]
            for(w = 0;w < G.vexnum; ++w)
                P[v][w] = FALSE;            //设空路径
            if(D[v] < INFINITY)
            {
                P[v][v0] = TRUE;
                P[v][v] = TRUE;
            }
        }
        D[v0]=0;
        final[v0]= TRUE; /* 初始化,v0顶点属于S集 */
        for(i = 1;i < G.vexnum; ++i) /* 其余G.vexnum-1个顶点 */
        { /* 开始主循环,每次求得v0到某个v顶点的最短路径,并加v到S集 */
            min = INFINITY; /* 当前所知离v0顶点的最近距离 */
            for(w = 0;w < G.vexnum; ++w)
                if(!final[w]) /* w顶点在V-S中 */
                    if(D[w] < min)
                    {
                        v = w;
                        min = D[w];
                    } /* w顶点离v0顶点更近 */
                    final[v] = TRUE; /* 离v0顶点最近的v加入S集 */
                    for(w = 0;w < G.vexnum; ++w) /* 更新当前最短路径及距离 */
                    {
                        if(!final[w] && min < INFINITY && G.arcs[v][w].adj < INFINITY && (min + G.arcs[v][w].adj < D[w]))
                        { /* 修改D[w]和P[w],w∈V-S */
                            D[w] = min + G.arcs[v][w].adj;
                            for(j = 0;j < G.vexnum;++j)
                                P[w][j] = P[v][j];
                            P[w][w] = TRUE;
                        }
                    }
        }
    }

     

    经典二分问题

    经典二分问题:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  。

    写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
    示例 1:

    输入: nums = [-1,0,3,5,9,12], target = 9。输出: 4
    解释: 9 出现在 nums 中并且下标为 4
    示例 2:

    输入: nums = [-1,0,3,5,9,12], target = 2。输出: -1
    解释: 2 不存在 nums 中因此返回 -1

    思路1:我们当然可以一个数一个数的遍历,但是毫无疑问要被大妈鄙视,这可怎么办呢?

    思路2:二分查找
    二分查找是一种基于比较目标值和数组中间元素的教科书式算法。

    如果目标值等于中间元素,则找到目标值。
    如果目标值较小,证明目标值小于中间元素及右边的元素,继续在左侧搜索。
    如果目标值较大,证明目标值大于中间元素及左边的元素,继续在右侧搜索。

    算法代码描述:

    初始化指针 left = 0, right = n - 1。
    当 left <= right:
    比较中间元素 nums[pivot] 和目标值 target 。
    如果 target = nums[pivot],返回 pivot。
    如果 target < nums[pivot],则在左侧继续搜索 right = pivot - 1。
    如果 target > nums[pivot],则在右侧继续搜索 left = pivot + 1。

    算法实现:照例贴出三种语言的实现,在Java实现中给出了详细注释

    class Solution {
      public int search(int[] nums, int target) {
    	//分别准备好左右端点
        int left = 0, right = nums.length - 1;
    	//循环二分
        while (left <= right) {
    	//取中点
          int pivot = left + (right - left) / 2;
    	  //找到答案并返回
          if (nums[pivot] == target) return pivot;
    	  //向左继续找
          if (target < nums[pivot]) right = pivot - 1;
    	  //向右继续找
          else left = pivot + 1;
        }
    	//未找到,返回-1
        return -1;
      }
    }
    class Solution:
        def search(self, nums: List[int], target: int) -> int:
            left, right = 0, len(nums) - 1
            while left <= right:
                pivot = left + (right - left) // 2
                if nums[pivot] == target:
                    return pivot
                if target < nums[pivot]:
                    right = pivot - 1
                else:
                    left = pivot + 1
            return -1
    class Solution {
      public:
      int search(vector<int>& nums, int target) {
        int pivot, left = 0, right = nums.size() - 1;
        while (left <= right) {
          pivot = left + (right - left) / 2;
          if (nums[pivot] == target) return pivot;
          if (target < nums[pivot]) right = pivot - 1;
          else left = pivot + 1;
        }
        return -1;
      }
    };

     

     

     

    二叉搜索树实现

    本文给出二叉搜索树介绍和实现

     

    首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。

     

    那这个结构有什么有用呢?

    首先可以快速二分查找。还可以中序遍历得到升序序列,等等。。。

    基本操作:

    1、插入某个数值

    2、查询是否包含某个数值

    3、删除某个数值

     

    根据实现不同,还可以实现其他很多种操作。

     

    实现思路思路:

    前两个操作很好想,就是不断比较,大了往左走,小了往右走。到空了插入,或者到空都没找到。

    而删除稍微复杂一些,有下面这几种情况:

    1、需要删除的节点没有左儿子,那就把右儿子提上去就好了。

    2、需要删除的节点有左儿子,这个左儿子没有右儿子,那么就把左儿子提上去

    3、以上都不满足,就把左儿子子孙中最大节点提上来。

     

    当然,反过来也是成立的,比如右儿子子孙中最小的节点。

     

    下面来叙述为什么可以这么做。

    下图中A为待删除节点。

    第一种情况:

     

    1、去掉A,把c提上来,c也是小于x的没问题。

    2、根据定义可知,x左边的所有点都小于它,把c提上来不影响规则。

     

    第二种情况

     

    3、B<A<C,所以B<C,根据刚才的叙述,B可以提上去,c可以放在b右边,不影响规则

    4、同理

     

    第三种情况

     

    5、注意:是把黑色的提升上来,不是所谓的最右边的那个,因为当初向左拐了,他一定小。

    因为黑色是最大,比B以及B所有的孩子都大,所以让B当左孩子没问题

    而黑点小于A,也就小于c,所以可以让c当右孩子

    大概证明就这样。。

    下面我们用代码实现并通过注释理解

    上次链表之类的用的c,循环来写的。这次就c++函数递归吧,不同方式练习。

    定义

    struct node
    {
        int val;//数据
        node *lch,*rch;//左右孩子
    };

    插入

     node *insert(node *p,int x)
     {
         if(p==NULL)//直到空就创建节点
         {
             node *q=new node;
             q->val=x;
             q->lch=q->rch=NULL;
             return p;
         }
         if(x<p->val)p->lch=insert(p->lch,x);
         else p->lch=insert(p->rch,x);
         return p;//依次返回自己,让上一个函数执行。
     }

    查找

     bool find(node *p,int x)
     {
         if(p==NULL)return false;
         else if(x==p->val)return true;
         else if(x<p->val)return find(p->lch,x);
         else return find(p->rch,x);
     }

    删除

     node *remove(node *p,int x)
     {
          if(p==NULL)return NULL;
          else if(x<p->val)p->lch=remove(p->lch,x);
          else if(x>p->val)p->lch=remove(p->rch,x);
          //以下为找到了之后
          else if(p->lch==NULL)//情况1
          {
              node *q=p->rch;
              delete p;
              return q;
          }
          else if(p->lch->rch)//情况2
          {
              node *q=p->lch;
              q->rch=p->rch;
              delete p;
              return q;
          }
          else
          {
              node *q;
              for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大节点的前一个
              node *r=q->rch;//最大节点
              q->rch=r->lch;//最大节点左孩子提到最大节点位置
              r->lch=p->lch;//调整黑点左孩子为B
              r->rch=p->rch;//调整黑点右孩子为c
              delete p;//删除
              return r;//返回给父
          }
          return p;
     }

     

    对数组排序可以说是编程基础中的基础,本文对八种排序方法做简要介绍并用python实现。

    代码中注释很全,适合复习和萌新学习。这是刚入学自己写的,可能难免比不上标准的写法,但是懒得改了。

    文末会放和排序相关的基本拓展总结链接。

    看不明白可以看群里视频

    注意排序实现的具体方式,不要用局部变量,否则占空间太多,和空间复杂度不符。

    好,我们开始。

    • 选择排序

    选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在待排序序列的起始位置,直到全部待排序的数据元素排完。时间复杂度O(N^2)

    for i in range(len(l)):#意义是第i个位置开始挑第i大(小)的元素
        for j in range(i,len(l)):#和其他待排序的元素比较
    	if l[j]<l[i]:#更大就交换
    	    l[j],l[i]=l[i],l[j]
    • 冒泡排序

    冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法

    它重复地走访过要排序的元素列,一次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换(一般进行n次即可,第n次一定会把第n小的元素放到正确位置)。

    这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。时间复杂度O(N^2)

    for i in range(len(l)-1):#下标和i无关,代表的只是第i次排序,最多需要len(l)-1次排序即可
        for j in range(len(l)-1):#遍历每一个元素
    	if l[j]<l[j+1]:#本元素比下一个元素小,就交换
    		l[j],l[j+1]=l[j+1],l[j]

     分析一下其实每次排序都会多一个元素已经确定了位置,不需要再次遍历。

    所以j循环可以改成len(l)-i-1

    时间复杂度没变。

     

    • 插入排序

    有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。

    for i in range(1,len(l)):#意义是第i个元素开始插入i之前的序列(已经有序)
        for j in range(i,0,-1):#只要比它之前的元素小就交换
    	if l[j]<l[j-1]:
    	    l[j],l[j-1]=l[j-1],l[j]
    	else:
                break#直到比前一个元素大

     

    • 归并排序

    速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列

    试想:假设已经有两个有序数列,分别存放在两个数组s,r中,我们如何把他们有序的合并在一起?

    归并排序就是在重复这样的过程,首先单个元素合并为含有两个元素的数组(有序),然后这种数组再和同类数组合并为四元素数组,以此类推,直到整个数组合并完毕。

    def gg(l,ll):#合并函数
        a,b=0,0
        k=[]#用来合并的列表
        while a<len(l) and b<len(ll):#两边都非空
            if l[a]<ll[b]:
                k.append(l[a])
                a=a+1
            elif l[a]==ll[b]:a=a+1#实现去重
            else:
                k.append(ll[b])
                b=b+1
        k=k+l[a:]+ll[b:]#加上剩下的
        return k
    
    def kk(p):#分到只剩一个元素就开始合并
        if len(p)<=1:
            return p
        a=kk(p[0:len(p)//2])#不止一个元素就切片
        b=kk(p[len(p)//2:])
        return gg(a,b)#返回排好序的一部分
    l=list(map(int,input().split(" ")))
    print(kk(l))
    • 快速排序

    快速排序(Quicksort)是对冒泡排序的一种改进。

    快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

    • 随机化快排

    快速排序的最坏情况基于每次划分对主元的选择。基本的快速排序选取第一个元素作为主元。这样在数组已经有序的情况下,每次划分将得到最坏的结果。比如1 2 3 4 5,每次取第一个元素,就退化为了O(N^2)。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。

    这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度

    进一步提升可以分割为三部分,即小于区,等于区,大于区,减小了递归规模,并克服了多元素相同的退化。

    def gg(a,b):
        global l
        if a>=b:#注意停止条件,我以前没加>卡了半小时
            return
        x,y=a,b
        import random#为了避免遇到基本有序序列退化,随机选点
        g=random.randint(a,b)
        l[g],l[y]=l[y],l[g]#交换选中元素和末尾元素
        while a<b:
            if l[a]>l[y]:#比目标元素大
                l[a],l[b-1]=l[b-1],l[a]#交换
                b=b-1#大于区扩大
                #注意:换过以后a不要加,因为新换过来的元素并没有判断过
            else:a=a+1#小于区扩大
        l[y],l[a]=l[a],l[y]#这时a=b
        #现在解释a和b:a的意义是小于区下一个元素
        #b是大于区的第一个元素
        gg(x,a-1)#左右分别递归
        gg(a+1,y)
    
    l=list(map(int,input().split(" ")))
    gg(0,len(l)-1)
    print(l)
    
    • 堆排序

    堆排序(HeapSort)是一树形选择排序。堆排序的特点是:在排序过程中,将R[l..n]看成是一棵完全二叉树顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或最小)的记录。

    由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。

    堆排序是就地排序,辅助空间为O(1).

    它是不稳定的排序方法。

    主要思想:维持一个大根堆(根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆,又称最大堆。注意:①堆中任一子树亦是堆。②以上讨论的堆实际上是二叉堆(Binary Heap),类似地可定义k叉堆。)

    第一步:通过调整原地建立大根堆

    第二步:每次交换堆顶和边界元素,并减枝,然后调整堆顶下沉到正确位置。

    def down(i,k):#在表l里的第i元素调整,k为边界
    
    #优先队列也是通过这种方式实现的
        global l
        while 2*i+2<k:#右孩子不越界
            lift,right=2*i+1,2*i+2
            m=max(l[i],l[lift],l[right])
            if m==l[i]:#不需要调
                break
            if m==l[lift]:#把最大的换上来
                l[i],l[lift]=l[lift],l[i]
                i=lift#目的节点下标更新
            else:#把最大的换上来
                l[i],l[right]=l[right],l[i]
                i=right#目的节点下标更新
        if 2*i+1<k:#判断左孩子
            if l[2*i+1]>l[i]:
                l[i],l[2*i+1]=l[2*i+1],l[i]
    
    def main():
        global l
        for j in range(1,len(l)+1):#调大根堆
            i=len(l)-j
            down(i,len(l))
        for i in range(len(l)-1,-1,-1):#排序
            l[i],l[0]=l[0],l[i]#最大和边界交换,剪枝
            down(0,i)
        print(l)
        
    l=list(map(int,input().split(" ")))
    main()
    
            
        
    
            
    
    • 桶排序

    桶排序不是基于比较的排序方法,只需对号入座。将相应的数字放进相应编号的桶即可。

    当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间o(n)

    对于海量有范围数据十分适合,比如全国高考成绩排序,公司年龄排序等等。

    l=list(map(int,input().split(" ")))
    n=max(l)-min(l)
    p=[0]*(n+1)#为了省空间
    for i in l:
        p[i-min(l)]=1#去重排序,做标记即可
    for i in range(n):
        if p[i]==1:#判断是否出现过
            print(i+min(l),end=" ")
    • 希尔排序

    希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。

    通过缩小有序步长来实现。

     

    def shell(arr):
     n=len(arr)#初始化步长
     h=1
     while h<n/3:
      h=3*h+1
     while h>=1:#判断,退出后就有序了。
      for i in range(h,n):
       j=i
       while j>=h and arr[j]<arr[j-h]:#判断是否交换
        arr[j], arr[j-h] = arr[j-h], arr[j]
        j-=h
      h=h//3#逐渐缩小步长
     print arr

    稳定性及时间复杂度

    排序稳定性概念:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

    时间复杂度:时间复杂度是同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。可以理解为和常数操作所成的一种关系(常数操作为O(1))

    空间复杂度类似。

    下面给出各类排序的对比图:

     

     

     

    • 基数排序

    因为桶排序是稳定的,基数排序就是很多次桶排序而已,按位进行桶排序即可。

    (个人认为桶排序名字不恰当,因为桶是先进后出,和稳定的算法正好反了,)

     

     

     

     

    总:

    比较排序和非比较排序

          常见的排序算法都是比较排序,非比较排序包括计数排序、桶排序和基数排序,非比较排序对数据有要求,因为数据本身包含了定位特征,所有才能不通过比较来确定元素的位置。

          比较排序的时间复杂度通常为O(n2)或者O(nlogn),比较排序的时间复杂度下界就是O(nlogn),而非比较排序的时间复杂度可以达到O(n),但是都需要额外的空间开销。

    • 若n较小(数据规模较小),插入排序或选择排序较好
    • 若数据初始状态基本有序(正序),插入、冒泡或随机快速排序为宜
    • 若n较大,则采用时间复杂度为O(nlogn)的排序方法:快速排序或堆排序
    • 快速排序是目前基于比较的排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
    • 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。

     

     

     

     

     

     

     

     

     

    展开全文
  • 数据结构基础概念篇

    万次阅读 多人点赞 2017-11-14 13:44:24
    数据结构一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些...数据项:数据可分割的最小单位。一个数据元素可由若干个数据项组成。 数据

    数据结构

    一些概念

    数据结构就是研究数据的逻辑结构物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。

    1. 数据:所有能被输入到计算机中,且能被计算机处理的符号的集合。是计算机操作的对象的总称。
    2. 数据元素:数据(集合)中的一个“个体”,数据及结构中讨论的基本单位
    3. 数据项:数据的不可分割的最小单位。一个数据元素可由若干个数据项组成。
    4. 数据类型:在一种程序设计语言中,变量所具有的数据种类。整型、浮点型、字符型等等

    5. 逻辑结构:数据之间的相互关系。

      • 集合 结构中的数据元素除了同属于一种类型外,别无其它关系。
      • 线性结构 数据元素之间一对一的关系
      • 树形结构 数据元素之间一对多的关系
      • 图状结构或网状结构 结构中的数据元素之间存在多对多的关系
    6. 物理结构/存储结构:数据在计算机中的表示。物理结构是描述数据具体在内存中的存储(如:顺序结构、链式结构、索引结构、哈希结构)等
    7. 在数据结构中,从逻辑上可以将其分为线性结构和非线性结构
    8. 数据结构的基本操作的设置的最重要的准则是,实现应用程序与存储结构的独立。实现应用程序是“逻辑结构”,存储的是“物理结构”。逻辑结构主要是对该结构操作的设定,物理结构是描述数据具体在内存中的存储(如:顺序结构、链式结构、索引结构、希哈结构)等。
    9. 顺序存储结构中,线性表的逻辑顺序和物理顺序总是一致的。但在链式存储结构中,线性表的逻辑顺序和物理顺序一般是不同的。

    10. 算法五个特性: 有穷性、确定性、可行性、输入、输出

    11. 算法设计要求:正确性、可读性、健壮性、高效率与低存储量需求。(好的算法)
    12. 算法的描述有伪程序、流程图、N-S结构图等。E-R图是实体联系模型,不是程序的描述方式。
    13. 设计算法在执行时间时需要考虑:算法选用的规模、问题的规模
    14. 时间复杂度:算法的执行时间与原操作执行次数之和成正比。时间复杂度有小到大:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)、O(n3)。幂次时间复杂度有小到大O(2n)、O(n!)、O(nn)
    15. 空间复杂度:若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间

    线性表

    线性表是一种典型的线性结构。头结点无前驱有一个后继,尾节点无后继有一个前驱。链表只能顺序查找,定位一个元素的时间为O(N),删除一个元素的时间为O(1)

    1. 线性表的顺序存储结构:把线性表的结点按逻辑顺序依次存放在一组地址连续的存储单元里。用这种方法存储的线性表简称顺序表。是一种随机存取的存储结构。顺序存储指内存地址是一块的,随机存取指访问时可以按下标随机访问,存储和存取是不一样的。如果是存储,则是指按顺序的,如果是存取,则是可以随机的,可以利用元素下标进行。数组比线性表速度更快的是:原地逆序、返回中间节点、选择随机节点。
      • 便于线性表的构造和任意元素的访问
      • 插入:插入新结点,之后结点后移。平均时间复杂度:O(n)
      • 删除:删除节点,之后结点前移。平均时间复杂度:O(n)
    2. 线性链表:用一组任意的存储单元来依次存放线性表的结点,这组存储单元即可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。因此,链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址。data域是数据域,用来存放结点的值。next是指针域(亦称链域),用来存放结点的直接后继的地址(或位置)。不需要事先估计存储空间大小。
      • 单链表中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。同时,由于最后一个结点无后继,故结点的指针域为空,即NULL。头插法建表(逆序)、尾插法建表(顺序)。增加头结点的目的是算法实现上的方便,但增大了内存开销。
        • 查找:只能从链表的头指针出发,顺链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构
        • 插入:先找到表的第i-1的存储位置,然后插入。新结点先连后继,再连前驱。
        • 删除:首先找到ai-1的存储位置p。然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间.r=p->next;p->next=r->next;delete r。
        • 判断一个单向链表中是否存在环的最佳方法是快慢指针。
      • 静态链表:用一维数组来实现线性链表,这种用一维数组表示的线性链表,称为静态链表。静态:体现在表的容量是一定的。(数组的大小);链表:插入与删除同前面所述的动态链表方法相同。静态链表中指针表示的是下一元素在数组中的位置。
      • 静态链表是用数组实现的,是顺序的存储结构,在物理地址上是连续的,而且需要预先分配大小。动态链表是用申请内存函数(C是malloc,C++是new)动态申请内存的,所以在链表的长度上没有限制。动态链表因为是动态申请内存的,所以每个节点的物理地址不连续,要通过指针来顺序访问。静态链表在插入、删除时也是通过修改指针域来实现的,与动态链表没有什么分别
      • 循环链表:是一种头尾相接的链表。其特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
        • 在单链表中,将终端结点的指针域NULL改为指向表头结点的或开始结点,就得到了单链形式的循环链表,并简单称为单循环链表。由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p—>next是否为空,而是判断它们是否等于某一指定指针,如头指针或尾指针等。
      • 双向链表:在单链表的每个结点里再增加一个指向其直接前趋的指针域prior。这样就形成的链表中有两个方向不同的链。双链表一般由头指针唯一确定的,将头结点和尾结点链接起来构成循环链表,并称之为双向链表。设指针p指向某一结点,则双向链表结构的对称性可用下式描述:p—>prior—>next=p=p—>next—>prior。从两个方向搜索双链表,比从一个方向搜索双链表的方差要小。
        • 插入:先搞定插入节点的前驱和后继,再搞定后结点的前驱,最后搞定前结点的后继。
        • 在有序双向链表中定位删除一个元素的平均时间复杂度为O(n)
        • 可以直接删除当前指针所指向的节点。而不需要像单向链表中,删除一个元素必须找到其前驱。因此在插入数据时,单向链表和双向链表操作复杂度相同,而删除数据时,双向链表的性能优于单向链表

    栈和队列

    栈(Stack)是限制在表的一端进行插入和删除运算的线性表,通常称插入、删除的这一端为栈顶(Top),另一端为栈底(Bottom)。先进后出。top= -1时为空栈,top=0只能说明栈中只有一个元素,并且元素进栈时top应该自增

    1. 顺序存储栈:顺序存储结构
    2. 链栈:链式存储结构。插入和删除操作仅限制在链头位置上进行。栈顶指针就是链表的头指针。通常不会出现栈满的情况。 不需要判断栈满但需要判断栈空。
    3. 两个栈共用静态存储空间,对头使用也存在空间溢出问题。栈1的底在v[1],栈2的底在V[m],则栈满的条件是top[1]+1=top[2]。
    4. 基本操作:删除栈顶元素、判断栈是否为空以及将栈置为空栈等
    5. 对于n各元素的入栈问题,可能的出栈顺序有C(2n,n)/(n+1)个。
    6. 堆栈溢出一般是循环的递归调用、大数据结构的局部变量导致的

    应用,代码

    1. 进制转换
    2. 括号匹配的检验
    3. 行编辑程序
    4. 迷宫求解:若当前位置“可通”,则纳入路径,继续前进;若当前位置“不可通”,则后退,换方向继续探索;若四周“均无通路”,则将当前位置从路径中删除出去。
    5. 表达式求解:前缀、中缀、后缀。
      • 操作数之间的相对次序不变;
      • 运算符的相对次序不同;
      • 中缀式丢失了括弧信息,致使运算的次序不确定
      • 前缀式的运算规则为:连续出现的两个操作数和在它们之前且紧靠它们的运算符构成一个最小表达式
      • 后缀式的运算规则为:运算符在式中出现的顺序恰为表达式的运算顺序;每个运算符和在它之前出现且紧靠它的两个操作数构成一个最小表达式。
    6. 实现递归:多个函数嵌套调用的规则是:后调用先返回。
    7. 浏览器历史纪录,Android中的最近任务,Activity的启动模式,CPU中栈的实现,Word自动保存,解析计算式,解析xml/json。解析XML时,需要校验节点是否闭合,节点闭合的话,有头尾符号相对应,遇到头符号将其放入栈中,遇到尾符号时,弹出栈的内容,看是否有与之对应的头符号,栈的特性刚好符合符号匹配的就近原则。

    不是所有的递归程序都需要栈来保护现场,比方说求阶乘的,是单向递归,直接用循环去替代从1乘到n就是结果了,另外一些需要栈保存的也可以用队列等来替代。不是所有的递归转化为非递归都要用到栈。转化为非递归主要有两种方法:对于尾递归或单向递归,可以用循环结构算法代替

    队列

    队列(Queue)也是一种运算受限的线性表。它只允许在表的一端进行插入,而在另一端进行删除。允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。先进先出。

    1. 顺序队列:顺序存储结构。当头尾指针相等时队列为空。在非空队列里,头指针始终指向队头前一个位置,而尾指针始终指向队尾元素的实际位置
    2. 循环队列。在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。只不过当头尾指针指向向量上界(MaxSize-1)时,其加1操作的结果是指向向量的下界0。除非向量空间真的被队列元素全部占用,否则不会上溢。因此,除一些简单的应用外,真正实用的顺序队列是循环队列。故队空和队满时头尾指针均相等。因此,我们无法通过front=rear来判断队列“空”还是“满”
    3. 链队列:链式存储结构。限制仅在表头删除和表尾插入的单链表。显然仅有单链表的头指针不便于在表尾做插入操作,为此再增加一个尾指针,指向链表的最后一个结点。
    4. 设尾指针的循环链表表示队列,则入队和出队算法的时间复杂度均为O(1)。用循环链表表示队列,必定有链表的头结点,入队操作在链表尾插入,直接插入在尾指针指向的节点后面,时间复杂度是常数级的;出队操作在链表表头进行,也就是删除表头指向的节点,时间复杂度也是常数级的。

    5. 队空条件:rear==front,但是一般需要引入新的标记来说明栈满还是栈空,比如每个位置布尔值

    6. 队满条件:(rear+1) % QueueSize==front,其中QueueSize为循环队列的最大长度
    7. 计算队列长度:(rear-front+QueueSize)% QueueSize
    8. 入队:(rear+1)% QueueSize
    9. 出队:(front+1)% QueueSize
    10. 假设以数组A[N]为容量存放循环队列的元素,其头指针是front,当前队列有X个元素,则队列的尾指针值为(front+X mod N)

    串(String)是零个或多个字符组成的有限序列。长度为零的串称为空串(Empty String),它不包含任何字符。通常将仅由一个或多个空格组成的串称为空白串(Blank String) 注意:空串和空白串的不同,例如“ ”和“”分别表示长度为1的空白串和长度为0的空串。

    串的表示和实现:

    1. 定长顺序存储表示。静态存储分配的顺序表。
    2. 堆分配存储表示。存储空间是在程序执行过程中动态分配而得。所以也称为动态存储分配的顺序表
    3. 串的链式存储结构。

    串匹配:将主串称为目标串,子串称之为模式串。蛮力法匹配。KMP算法匹配。Boyer-Moore算法匹配。

    数组和广义表

    数组和广义表可看成是一种特殊的线性表,其特殊在于: 表中的元素本身也是一种线性表。内存连续。根据下标在O(1)时间读/写任何元素。

    二维数组,多维数组,广义表、树、图都属于非线性结构

    数组

    数组的顺序存储:行优先顺序;列优先顺序。数组中的任一元素可以在相同的时间内存取,即顺序存储的数组是一个随机存取结构。

    关联数组(Associative Array),又称映射(Map)、字典( Dictionary)是一个抽象的数据结构,它包含着类似于(键,值)的有序对。 不是线性表。

    矩阵的压缩:

    1. 对称矩阵、三角矩阵:直接存储矩阵的上三角或者下三角元素。注意区分i>=j和i

    广义表

    广义表(Lists,又称列表)是线性表的推广。广义表是n(n≥0)个元素a1,a2,a3,…,an的有限序列,其中ai或者是原子项,或者是一个广义表。若广义表LS(n>=1)非空,则a1是LS的表头,其余元素组成的表(a2,…an)称为LS的表尾。广义表的元素可以是广义表,也可以是原子,广义表的元素也可以为空。表尾是指除去表头后剩下的元素组成的表,表头可以为表或单元素值。所以表尾不可以是单个元素值。

    例子:

    1. A=()——A是一个空表,其长度为零。
    2. B=(e)——表B只有一个原子e,B的长度为1。
    3. C=(a,(b,c,d))——表C的长度为2,两个元素分别为原子a和子表(b,c,d)。
    4. D=(A,B,C)——表D的长度为3,三个元素都是广义 表。显然,将子表的值代入后,则有D=(( ),(e),(a,(b,c,d)))。
    5. E=(a,E)——这是一个递归的表,它的长度为2,E相当于一个无限的广义表E=(a,(a,(a,(a,…)))).

    三个结论:

    1. 广义表的元素可以是子表,而子表的元素还可以是子表。由此,广义表是一个多层次的结构,可以用图形象地表示
    2. 广义表可为其它表所共享。例如在上述例4中,广义表A,B,C为D的子表,则在D中可以不必列出子表的值,而是通过子表的名称来引用。
    3. 广义表的递归性

    考点:

    1. 广义表是0个或多个单因素或子表组成的有限序列,广义表可以是自身的子表,广义表的长度n>=0,所以可以为空表。广义表的同级元素(直属于同一个表中的各元素)具有线性关系
    2. 广义表的表头为空,并不代表该广义表为空表。广义表()和(())不同。前者是长度为0的空表,对其不能做求表头和表尾的运算;而后者是长度为l的非空表(只不过该表中惟一的一个元素是空表),对其可进行分解,得到的表头和表尾均是空表()
    3. 已知广义表LS=((a,b,c),(d,e,f)),运用head和tail函数取出LS中原子e的运算是head(tail(head(tail(LS)))。根据表头、表尾的定义可知:任何一个非空广义表的表头是表中第一个元素,它可以是原子,也可以是子表,而其表尾必定是子表。也就是说,广义表的head操作,取出的元素是什么,那么结果就是什么。但是tail操作取出的元素外必须加一个表——“()“。tail(LS)=((d,e,f));head(tail(LS))=(d,e,f);tail(head(tail(LS)))=(e,f);head(tail(head(tail(LS))))=e。
    4. 二维以上的数组其实是一种特殊的广义表
    5. 在(非空)广义表中:1、表头head可以是原子或者一个表 2、表尾tail一定是一个表 3.广义表难以用顺序存储结构 4.广义表可以是一个多层次的结构

    树和二叉树

    一种非线性结构。树是递归结构,在树的定义中又用到了树的概念。

    基本术语:

    1. 树结点:包含一个数据元素及若干指向子树的分支;
    2. 孩子结点:结点的子树的根称为该结点的孩子;
    3. 双亲结点:B结点是A结点的孩子,则A结点是B结点的双亲;
    4. 兄弟结点:同一双亲的孩子结点;
    5. 堂兄结点:同一层上结点;
    6. 结点层次:根结点的层定义为1;根的孩子为第二层结点,依此类推;
    7. 树的高(深)度:树中最大的结点层
    8. 结点的度:结点子树的个数
    9. 树的度: 树中最大的结点度。
    10. 叶子结点:也叫终端结点,是度为0的结点;
    11. 分枝结点:度不为0的结点(非终端结点);
    12. 森林:互不相交的树集合;
    13. 有序树:子树有序的树,如:家族树;
    14. 无序树:不考虑子树的顺序;

    二叉树

    二叉树可以为空。二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树,还是右子树。这是二叉树与树的最主要的差别。注意区分:二叉树、二叉查找树/二叉排序树/二叉搜索树二叉平衡(查找)树

    二叉平衡树肯定是一颗二叉排序树。堆不是一颗二叉平衡树。

    二叉树与树是不同的,二叉树不等价于分支树最多为二的有序树。当一个结点只包含一个子节点时,对于有序树并无左右孩子之分,而对于二叉树来说依然有左右孩子之分,所以二叉树与树是两种不同的结构。

    性质:

    1. 在二叉树的第 i 层上至多有2i-1个结点。
    2. 深度为 k 的二叉树上至多含 2k-1 个结点(k≥1)
    3. 对任何一棵二叉树,若它含有n0个叶子结点、n2个度为 2 的结点,则必存在关系式:n0= n2+1。
    4. 具有 n 个结点的完全二叉树的深度为⎣log2 n⎦+1 。
    5. n个结点的二叉树中,完全二叉树具有最小的路径长度。
    6. 如果对一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点i(1<=i<=n),有:
      • 如果i=1,则结点i无双亲,是二叉树的根;如果i>1,则其双亲的编号是 i/2(整除)。
      • 如果2i>n,无左孩子;否则,其左孩子是结点2i。
      • 如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1。

    二叉树的存储结构

    1. 顺序存储结构:仅仅适用于满或完全二叉树,结点之间的层次关系由性质5确定。
    2. 二叉链表法:每个节点存储左子树和右子树。三叉链表:左子树、右子树、父节点,总的指针是n+2
    3. 在有n个结点的二叉链表中,值为非空的链域的个数为n-1。在有N个结点的二叉链表中必定有2N个链域。除根结点外,其余N-1个结点都有一个父结点。所以,一共有N-1个非空链域,其余2N-(N-1)=N+1个为空链域。
    4. 二叉链存储法也叫孩子兄弟法,左指针指向左孩子,右指针指向右兄弟。而中序遍历的顺序是左孩子,根,右孩子。这种遍历顺序与存储结构不同,因此需要堆栈保存中间结果。而中序遍历检索二叉树时,由于其存储结构跟遍历顺序相符,因此不需要用堆栈。

    遍历二叉树和线索二叉树

    遍历二叉树:使得每一个结点均被访问一次,而且仅被访问一次。非递归的遍历实现要利用栈。

    • 先序遍历DLR:根节点->左子树->右子树
    • 中序遍历LDR:左子树->根节点->右子树。必须要有中序遍历才能得到一棵二叉树的正确顺序
    • 后续遍历LRD:左子树->右子树->根节点。需要栈的支持。
    • 层次遍历:用一维数组存储二叉树时,总是以层次遍历的顺序存储结点。层次遍历应该借助队列。

    线索二叉树:对二叉树所有结点做某种处理可在遍历过程中实现;检索(查找)二叉树某个结点,可通过遍历实现;如果能将二叉树线索化,就可以简化遍历算法,提高遍历速度,目的是加快查找结点的前驱或后继的速度。

    如何线索化?以中序遍历为例,若能将中序序列中每个结点前趋、后继信息保存起来,以后再遍历二叉树时就可以根据所保存的结点前趋、后继信息对二叉树进行遍历。对于二叉树的线索化,实质上就是遍历一次二叉树,只是在遍历的过程中,检查当前结点左,右指针域是否为空,若为空,将它们改为指向前驱结点或后继结点的线索。前驱就是在这一点之前走过的点,不是下一将要去往的点

    加上结点前趋后继信息(结索)的二叉树称为线索二叉树。n个结点的线索二叉树上每个结点有2个指针域(指向左孩子和右孩子),总共有2n个指针域;一个n个结点的树有n-1条边,那么空指针域= 2n - (n-1) = n + 1,即线索数为n+1。指针域tag为0,存放孩子指针,为1,存放前驱/后继节点指针。

    线索树下结点x的前驱与后继查找:设结点x相应的左(右)标志是线索标志,则lchild(rchild)就是前驱(后继),否则:

    • LDR–前驱:左子树中最靠右边的结点;后继:右子树中最靠左边的结点
    • LRD–前驱:右子树的根,若无右子树,为左子树跟。后继:x是根,后继是空;x是双亲的右孩子、x是双亲的左孩子,但双亲无右孩子,双亲是后继;x是双亲的左孩子,双亲有右孩子,双亲右子树中最左的叶子是后继
    • DLR–对称于LRD线索树—将LRD中所有左右互换,前驱与后继互换,得到DLR的方法。
    • 为简化线索链表的遍历算法,仿照线性链表,为线索链表加上一头结点,约定:
      • 头结点的lchild域:存放线索链表的根结点指针;
      • 头结点的rchild域: 中序序列最后一个结点的指针;
      • 中序序列第一结点lchild域指向头结点;
      • 中序序列最后一个结点的rchild域指向头结点;

    中序遍历的线索二叉树以及线索二叉树链表示意图
    xiansuobinarytree

    一棵左右子树均不空的二叉树在前序线索化后,其中空的链域的个数是1。前序和后续线索化后空链域个数都是1,中序是2。二叉树在线索化后,仍不能有效求解的问题是前序求前序先驱,后序求后序后继。

    中序遍历的顺序为:左、根、右,所以对于每一非空的线索,左子树结点的后继为根结点,右子树结点的前驱为根结点,再递归的执行上面的过程,可得非空线索均指向其祖先结点。在中序线索二叉树中,每一非空的线索均指向其祖先结点

    在二叉树上加上结点前趋、后继线索后,可利用线索对二叉树进行遍历,此时,不需栈,也不需递归。基本步骤:

    1. p=T->lchild; p指向线索链表的根结点;
    2. 若线索链表非空,循环:
      • 循环,顺着p左孩子指针找到最左下结点;访问之;
      • 若p所指结点的右孩子域为线索,p的右孩子结点即为后继结点循环: p=p->rchild; 并访问p所指结点;(在此循环中,顺着后继线索访问二叉树中的结点)
      • 一旦线索“中断”,p所指结点的右孩子域为右孩子指针,p=p->rchild,使 p指向右孩子结点;

    树和森林

    树的存储结构:

    1. 双亲表示法
    2. 孩子表示法
    3. 利用图表示树
    4. 孩子兄弟表示法(二叉树表示法):链表中每个结点的两指针域分别指向其第一个孩子结点和下一个兄弟结点

    将树转化成二叉树:右子树一定为空

    1. 加线:在兄弟之间加一连线
    2. 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
    3. 旋转:以树的根结点为轴心,将整树顺时针转45°

    森林转换成二叉树:

    1. 将各棵树分别转换成二叉树
    2. 将每棵树的根结点用线相连
    3. 以第一棵树根结点为二叉树的根

    树与转换后的二叉树的关系:转换后的二叉树的先序对应树的先序遍历;转换后的二叉树的中序对应树的后序遍历

    哈弗曼树/霍夫曼树

    一些概念

    1. 路径:从一个祖先结点到子孙结点之间的分支构成这两个结点间的路径;
    2. 路径长度:路径上的分支数目称为路径长度;
    3. 树的路径长度:从根到每个结点的路径长度之和。
    4. 结点的权:根据应用的需要可以给树的结点赋权值;
    5. 结点的带权路径长度:从根到该结点的路径长度与该结点权的乘积;
    6. 树的带权路径长度=树中所有叶子结点的带权路径之和;通常记作 WPL=∑wi×li
    7. 哈夫曼树:假设有n个权值(w1, w2, … , wn),构造有n个叶子结点的二叉树,每个叶子结点有一个 wi作为它的权值。则带权路径长度最小的二叉树称为哈夫曼树。最优二叉树。

    前缀码的定义:在一个字符集中,任何一个字符的编码都不是另一个字符编码的前缀。霍夫曼编码就是前缀码,可用于快速判断霍夫曼编码是否正确。霍夫曼树是满二叉树,若有n个节点,则共有(n+1)/2个码子

    给定n个权值作为n的叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为霍夫曼树(Huffman Tree)。霍夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

    假设哈夫曼树是二叉的话,则度为0的结点个数为N,度为2的结点个数为N-1,则结点总数为2N-1。哈夫曼树的结点个数必为奇数。

    哈夫曼树不一定是完全二叉树,但一定是最优二叉树。

    若度为m的哈夫曼树中,其叶结点个数为n,则非叶结点的个数为[(n-1)/(m-1)]。边的数目等于度。

    图遍历与回溯

    图搜索->形成搜索树

    1. 穷举法。
    2. 贪心法。多步决策,每步选择使得构成一个问题的可能解,同时满足目标函数。
    3. 回溯法。根据题意,选取度量标准,然后将可能的选择方法按度量标准所要求顺序排好,每次处理一个量,得到该意义下的最优解的分解处理。

    无向图

    1. 回路或环:第一个顶点和最后一个顶点相同的路径。
    2. 简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路
    3. 连通:顶点v至v’ 之间有路径存在
    4. 连通图:无向图图 G 的任意两点之间都是连通的,则称G是连通图。
    5. 连通分量:极大连通子图,子图中包含的顶点个数极大
    6. 所有顶点度的和必须为偶数

    有向图:

    1. 回路或环:第一个顶点和最后一个顶点相同的路径。
    2. 简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路。
    3. 连通:顶点v至v’之间有路径存在
    4. 强连通图:有向图G的任意两点之间都是连通的,则称G是强连通图。各个顶点间均可达。
    5. 强连通分量:极大连通子图
    6. 有向图顶点的度是顶点的入度与出度之和。邻接矩阵中第V行中的1的个数是V的出度

    7. 生成树:极小连通子图。包含图的所有n个结点,但只含图的n-1条边。在生成树中添加一条边之后,必定会形成回路或环。

    8. 完全图:有 n(n-1)/2 条边的无向图。其中n是结点个数。必定是连通图。
    9. 有向完全图:有n(n-1)条边的有向图。其中n是结点个数。每两个顶点之间都有两条方向相反的边连接的图。
    10. 一个无向图 G=(V,E) 是连通的,那么边的数目大于等于顶点的数目减一:|E|>=|V|-1,而反之不成立。如果 G=(V,E) 是有向图,那么它是强连通图的必要条件是边的数目大于等于顶点的数目:|E|>=|V|,而反之不成立。没有回路的无向图是连通的当且仅当它是树,即等价于:|E|=|V|-1。

    图的存储形式

    1. 邻接矩阵和加权邻接矩阵
      • 无权有向图:出度: i行之和;入度: j列之和。
      • 无权无向图:i结点的度: i行或i列之和。
      • 加权邻接矩阵:相连为w,不相连为∞
    2. 邻接表
      • 用顶点数组表、边(弧)表表示该有向图或无向图
      • 顶点数组表:用数组存放所有的顶点。数组大小为图顶点数n
      • 边表(边结点表):每条边用一个结点进行表示。同一个结点的所有的边形成它的边结点单链表。
      • n个顶点的无向图的邻接表最多有n(n-1)个边表结点。有n个顶点的无向图最多有n*(n-1)/2条边,此时为完全无向图,而在邻接表中每条边存储两次,所以有n*(n-1)个结点

    图的遍历

    深度优先搜索利用栈,广度优先搜索利用队列

    求一条从顶点i到顶点s的简单路径–深搜。求两个顶点之间的一条长度最短的路径–广搜。当各边上的权值均相等时,BFS算法可用来解决单源最短路径问题。

    生成树和最小生成树

    每次遍历一个连通图将图的边分成遍历所经过的边和没有经过的边两部分,将遍历经过的边同图的顶点构成一个子图,该子图称为生成树。因此有DFS生成树和BFS生成树。

    生成树是连通图的极小子图,有n个顶点的连通图的生成树必定有n-1条边,在生成树中任意增加一条边,必定产生回路。若砍去它的一条边,就会把生成树变成非连通子图

    最小生成树:生成树中边的权值(代价)之和最小的树。最小生成树问题是构造连通网的最小代价生成树。

    Kruskal算法:令最小生成树集合T初始状态为空,在有n个顶点的图中选取代价最小的边并从图中删去。若该边加到T中有回路则丢弃,否则留在T中;依此类推,直至T中有n-1条边为止。

    Prim算法、Kruskal算法和Dijkstra算法均属于贪心算法。

    1. Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为非负值。
    2. Dijkstra算法解决了从某个原点到其余各顶点的最短路径问题,由循环嵌套可知该算法的时间复杂度为O(N*N)。若要求任一顶点到其余所有顶点的最短路径,一个比较简单的方法是对每个顶点当做源点运行一次该算法,等于在原有算法的基础上,再来一次循环,此时整个算法的复杂度就变成了O(N*N*N)。
    3. Bellman-Ford算法解决的是一般情况下的单源最短路径问题,在这里,边的权重可以为负值。该算法返回一个布尔值,以表明是否存在一个从源节点可以到达的权重为负值的环路。如果存在这样一个环路,算法将告诉我们不存在解决方案。如果没有这种环路存在,算法将给出最短路径和它们的权重。

    双连通图和关节点

    若从一个连通图中删去任何一个顶点及其相关联的边,它仍为一个连通图的话,则该连通图被称为重(双)连通图

    若连通图中的某个顶点和其相关联的边被删去之后,该连通图被分割成两个或两个以上的连通分量,则称此顶点为关节点

    没有关节点的连通图为双连通图

    1. 若生成树的根结点,有两个或两个以上的分支,则此顶点(生成树的根)必为关节点;
    2. 对生成树上的任意一个非叶“顶点”,若其某棵子树中的所有“顶点”没有和其祖先相通的回边,则该“顶点”必为关节点。

    有向无环图及其应用

    拓扑排序。在用邻接表表示图时,对有n个顶点和e条弧的有向图而言时间复杂度为O(n+e)。一个有向图能被拓扑排序的充要条件就是它是一个有向无环图。拓扑序列唯一不能唯一确定有向图。

    AOV网(Activity On Vertex):用顶点表示活动,边表示活动的优先关系的有向图称为AOV网。AOV网中不允许有回路,这意味着某项活动以自己为先决条件。

    拓扑有序序列:把AOV网络中各顶点按照它们相互之间的优先关系排列一个线性序列的过程。若vi是vj前驱,则vi一定在vj之前;对于没有优先关系的点,顺序任意。

    拓扑排序:对AOV网络中顶点构造拓扑有序序列的过程。方法:

    1. 在有向图中选一个没有前驱的顶点且输出之
    2. 从图中删除该顶点和所有以它为尾的弧
    3. 重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止(此时说明图中有环)

    采用深度优先搜索拓扑排序算法可以判断出一个有向图中是否有环(回路).深度优先搜索只要在其中记录下搜索的节点数n,当n大于图中节点数时退出,并可以得出有回路。若有回路,则拓扑排序访问不到图中所有的节点,所以也可以得出回路。广度优先搜索过程中如果访问到一个已经访问过的节点,可能是多个节点指向这个节点,不一定是存在环。

    算法描述:

    1. 把邻接表中入度为0的顶点依此进栈
    2. 若栈不空,则
      • 栈顶元素vj退栈并输出;
      • 在邻接表中查找vj的直接后继vk,把vk的入度减1;若vk的入度为0则进栈
    3. 若栈空时输出的顶点个数不是n,则有向图有环;否则,拓扑排序完毕。

    AOE网:带权的有向无环图,其中顶点表示事件,弧表示活动,权表示活动持续时间。在工程上常用来表示工程进度计划。

    一些定义:

    1. 事件的最早发生时间(ve(j)):从源点到j结点的最长的路径。意味着事件最早能够发生的时间。
    2. 事件的最迟发生时间(vl(j)):不影响工程的如期完工,事件j必须发生的时间。
    3. 活动ai由弧

    查找

    顺序查找、折半查找、索引查找、分块查找是静态查找,动态查找有二叉排序树查找,最优二叉树查找,键树查找,哈希表查找

    静态查找表

    顺序表的顺序查找:应用范围:顺序表或线性链表表示的表,表内元素之间无序。查找过程:从表的一端开始逐个进行记录的关键字和给定值的比较。

    顺序有序表的二分查找。平均查找时间(n+1)/n log2(n+1)

    分块查找:将表分成几块,块内无序,块间有序,即前一块中的最大值小于后一块中的最小值。并且有一张索引表,每一项存放每一块的最大值和指向该块第一个元素的指针。索引表有序,块内无序。所以,块间查找用二分查找,块内用顺序查找,效率介于顺序和二分之间;先确定待查记录所在块,再在块内查找。因此跟表中元素个数和块中元素个数都有关。

    1. 用数组存放待查记录,
    2. 建立索引表,由每块中最大(小)的关键字及所属块位置的信息组成。
    3. 当索引表较大时,可以采用二分查找
    4. 在数据量极大时,索引可能很多,可考虑建立索引表的索引,即二级索引,原则上索引不超过三级

    分块查找平均查找长度:ASLbs = Lb + Lw。其中,Lb是查找索引表确定所在块的平均查找长度, Lw是在块中查找元素的平均查找长度。在n一定时,可以通过选择s使ASL尽可能小。当s=sqrt(n)时,ASL最小。

    1. 时间:顺序查找最差,二分最好,分块介于两者之间
    2. 空间:分块最大,需要增加索引数据的空间
    3. 顺序查找对表没有特殊要求
    4. 分块时数据块之间在物理上可不连续。所以可以达到插入、删除数据只涉及对应的块;另外,增加了索引的维护。
    5. 二分查找要求表有序,所以若表的元素的插入与删除很频繁,维持表有序的工作量极大。
    6. 在表不大时,一般直接使用顺序查找。

    动态查找

    二叉排序树的结点删除:

    1. x为叶子结点,则直接删除
    2. x只有左子树xL或只有右子树xR ,则令xL或xR直接成为双亲结点f的子树;
    3. x即有左子树xL也有右子树xR,在xL中选值最大的代替x,该数据按二叉排序树的性质应在最右边。

    平衡二叉树:每个结点的平衡因子都为 1、-1、0 的二叉排序树。或者说每个结点的左右子树的高度最多差1的二叉排序树。

    平衡二叉树的平衡:

    1. 左调整(新结点插入在左子树上的调整):
      • LL(插入在结点左子树的左子树上):旋转前后高度都为h+1
      • LR(新插入结点在左子树的右子树上):旋转前后高度仍为h+1
    2. 右调整(新结点插入在右子树上进行的调整):
      • RR(插入在的右子树的右子树上):处理方法和 LL对称
      • RL(插入在的右子树的左子树上):处理方法和 LR对称

    平衡树建立方法:

    1. 按二叉排序树插入结点
    2. 如引起结点平衡因子变为|2|,则确定旋转点,该点是离根最远(或最接近于叶子的点)
    3. 确定平衡类型后进行平衡处理,平衡后以平衡点为根的子树高不变
    4. 最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1 这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。

    常见的平衡二叉树:

    1. 红黑树是平衡二叉树,也就是左右子树是平衡的,高度大概相等。这种情况等价于一块完全二叉树的高度,查找的时间复杂度是树的高度,为logn,插入操作的平均时间复杂度为O(logn),最坏时间复杂度为O(logn)
      红黑树
      • 节点是红色或黑色。
      • 根是黑色。
      • 所有叶子都是黑色(叶子是NIL节点)。
      • 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
      • 从任一节点到其每个叶子的所有简单路径 都包含相同数目的黑色节点。
    2. avl树也是自平衡二叉树;红黑树和AVL树查找、插入、删除的时间复杂度相同;包含n个内部结点的红黑树的高度是o(logn); TreeMap 是一个红黑树的实现,能保证插入的值保证排序
    3. STL和linux多使用红黑树作为平衡树的实现:
      1. 如果插入一个node引起了树的不平衡,AVL和RB-Tree都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而RB-Tree最多只需3次旋转,只需要O(1)的复杂度。
      2. 其次,AVL的结构相较RB-Tree来说更为平衡,在插入和删除node更容易引起Tree的unbalance,因此在大量数据需要插入或者删除时,AVL需要rebalance的频率会更高。因此,RB-Tree在需要大量插入和删除node的场景下,效率更高。自然,由于AVL高度平衡,因此AVL的search效率更高。
      3. map的实现只是折衷了两者在search、insert以及delete下的效率。总体来说,RB-tree的统计性能是高于AVL的。

    查找总结

    1. 既希望较快的查找又便于线性表动态变化的查找方法是哈希法查找。二叉排序树查找,最优二叉树查找,键树查找,哈希法查找是动态查找。分块、顺序、折半、索引顺序查找均为静态。分块法应该是将整个线性表分成若干块进行保存,若动态变化则可以添加在表的尾部(非顺序结构),时间复杂度是O(1),查找复杂度为O(n);若每个表内部为顺序结构,则可用二分法将查找时间复杂度降至O(logn),但同时动态变化复杂度则变成O(n);顺序法是挨个查找,这种方法最容易实现,不过查找时间复杂度都是O(n),动态变化时可将保存值放入线性表尾部,则时间复杂度为O(1);二分法是基于顺序表的一种查找方式,时间复杂度为O(logn);通过哈希函数将值转化成存放该值的目标地址,O(1)
    2. 二叉树的平均查找长度为O(log2n)——O(n).二叉排序树的查找效率与二叉树的高度有关,高度越低,查找效率越高。二叉树的查找成功的平均查找长度ASL不超过二叉树的高度。二叉树的高度与二叉树的形态有关,n个节点的完全二叉树高度最小,高度为[log2n]+1,n个节点的单只二叉树的高度最大,高度为n,此时查找成功的ASL为最大(n+1)/2,因此二叉树的高度范围为[log2n]+1——n.
    3. 链式存储不能随机访问,必须是顺序存储

    B_树的B+树

    B_树

    B-树就是B树。m阶B_树满足或空,或为满足下列性质的m叉树:

    B-树

    1. 树中每个结点最多有m棵子树
    2. 根结点在不是叶子时,至少有两棵子树
    3. 除根外,所有非终端结点至少有⎡m/2⎤棵子树
    4. 有s个子树的非叶结点具有 n = s-1个关键字,结点的信息组织为:(n,A0,K1,A1,K2,A2 … Kn,An)。这里:n为关键字的个数,ki(i=1,2,…,n)为关键字,且满足Ki小于Ki+1,,Ai(i=0,1,..n)为指向子树的指针。
    5. 所有的叶子结点都出现在同一层上,不带信息(可认为外部结点或失败结点)。
    6. 关键字集合分布在整颗树中
    7. 任何一个关键字出现且只出现在一个结点中
    8. 搜索有可能在非叶子结点结束
    9. 其搜索性能等价于在关键字全集内做一次二分查找
    10. 只适用于随机检索,不适用于顺序检索。
    11. 有结点的平衡因子都为零
    12. M阶B-树中含有N个关键字,最大深度为log⎡m/2⎤(n+1)/2+2

    B_树中结点的插入

    1. m代表B_树的阶,插入总发生在最低层
    2. 插入后关键字个数小于等于 m-1,完成。
    3. 插入后关键字个数等于m,结点分裂,以中点数据为界一分为二,中点数据放到双亲结点中。这样就有可能使得双亲结点的数据个数为m,引起双亲结点的分裂,最坏情况下一直波及到根,引起根的分裂——B_树长高。

    3阶B_树的插入。每个结点最多3棵子树,2个数据;最少2棵子树,1个数据。所以3阶B_树也称为2-3树。

    B_树中结点的删除

    1. 删除发生在最底层
      • 被删关键字所在结点中的关键字数目大于等于 m/2 ,直接删除。
      • 删除后结点中数据为⎡m/2⎤-2,而相邻的左(右)兄弟中数据大于⎡m/2⎤-1,此时左(右兄弟)中最大(小)的数据上移到双亲中,双亲中接(靠)在它后(前)面的数据移到被删数据的结点中
      • 其左右兄弟结点中数据都是⎡m/2⎤-1,此时和左(右)兄弟合并,合并时连同双亲中相关的关键字。此时,双亲中少了一项,因此又可能引起双亲的合并,最坏一直到根,使B-树降低一层。
    2. 删除不在最底层
      • 在大于被删数据中选最小的代替被删数据,问题转换成在最底层的删除

    B+树

    在实际的文件系统中,用的是B+树或其变形。有关性质与操作类似与B_树。

    B+树

    差异:

    1. 有n棵子树的结点中有n个关键字,每个关键字不保存数据,只用来索引,所有数据都保存在叶子节点。
    2. 所有叶子结点中包含全部关键字信息,及对应记录位置信息及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。(而B树的叶子节点并没有包括全部需要查找的信息)
    3. 所有非叶子为索引,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B树的非终节点也包含需要查找的有效信息)
    4. 非叶最底层顺序联结,这样可以进行顺序查找

    B+特性

    1. 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
    2. 不可能在非叶子结点命中
    3. 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
    4. 更适合文件索引系统
    5. B+树插入操作的平均时间复杂度为O(logn),最坏时间复杂度为O(logn)

    查找过程

    • 在 B+ 树上,既可以进行缩小范围的查找,也可以进行顺序查找;
    • 在进行缩小范围的查找时,不管成功与否,都必须查到叶子结点才能结束;
    • 若在结点内查找时,给定值≤Ki, 则应继续在 Ai 所指子树中进行查找

    插入和删除的操作:类似于B_树进行,即必要时,也需要进行结点的“分裂”或“合并”。

    为什么说B+tree比B树更适合实际应用中操作系统的文件索引和数据库索引?

    1. B+tree的磁盘读写代价更低
      • B+tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
      • 举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B树就比B+树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
    2. B+tree的查询效率更加稳定
      • 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

    B树和B+树都是平衡的多叉树。B树和B+树都可用于文件的索引结构。B树和B+树都能有效的支持随机检索。B+树既能索引查找也能顺序查找.

    哈希表

    1. 在记录的存储地址和它的关键字之间建立一个确定的对应关系;这样不经过比较,一次存取就能得到元素。
    2. 哈希函数——在记录的关键字与记录的存储位置之间建立的一种对应关系。是从关键字空间到存储位置空间的一种映象。
    3. 哈希表——应用哈希函数,由记录的关键字确定记录在表中的位置信息,并将记录根据此信息放入表中,这样构成的表叫哈希表。
    4. Hash查找适合于关键字可能出现的值的集合远远大于实际关键字集合的情形。
    5. 更适合查找,不适合频繁更新
    6. Hash表等查找复杂依赖于Hash值算法的有效性,在最好的情况下,hash表查找复杂度为O(1)。只有无冲突的hash_table复杂度才是O(1)。一般是O(c),c为哈希关键字冲突时查找的平均长度。插入,删除,查找都是O(1)。平均查找长度不随表中结点数目的增加而增加,而是随负载因子的增大而增大
    7. 由于冲突的产生,使得哈希表的查找过程仍然是一个给定值与关键字比较的过程。

    根据抽屉原理,冲突是不可能完全避免的,所以,选择好的散列函数和冲突处理方法:

    1. 构造一个性能好,冲突少的Hash函数
    2. 如何解决冲突

    常用的哈希函数

    1. 直接定址法。仅适合于:地址集合的大小 == 关键字集合的大小
    2. 数字分析法。对关键字进行分析,取关键字的若干位或其组合作哈希地址。仅适合于:能预先估计出全体关键字的每一位上各种数字出现的频度。
    3. 平方取中法。以关键字的平方值的中间几位作为存储地址。
    4. 折叠法。将关键字分割成位数相同的几部分,然后取这几部分的叠加和(舍去进位)做哈希地址。移位叠加/间界叠加。适合于: 关键字的数字位数特别多,且每一位上数字分布大致均匀情况。
    5. 除留余数法。取关键字被某个不大于哈希表表长m的数p除后所得余数作哈希地址,即H(key)=key%p,p<=m。
    6. 随机数法。取关键字的伪随机函数值作哈希地址,即H(key)=random(key),适于关键字长度不等的情况。

    冲突解决

    1. 开放定址法。当冲突发生时,形成一个探查序列;沿此序列逐个地址探查,直到找到一个空位置(开放的地址),将发生冲突的记录放到该地址中。即Hi=(H(key)+di) % m,i=1,2,……k(k<=m-1),H(key)哈希函数,m哈希表长,di增量序列。缺点:删除:只能作标记,不能真正删除;溢出;载因子过大、解决冲突的算法选择不好会发生聚集问题。要求装填因子α较小,故当结点规模较大时会浪费很多空间。
      • 线性探测再散列:di=1,2,3,…,m-1
      • 二次探测再散列:di=12,-12,22,-22,…,±k2(k<=m/2)
      • 伪随机探测再散列: di为伪随机数序列
    2. 链地址法:将所有关键字为同义词的记录存储在一个单链表中,并用一维数组存放头指针。拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间。一旦发生冲突,在当前位置给单链表增加结点就行。
    3. 其他方法:再哈希法、建立公共溢出区
    4. 在用拉链法构造的散列表中,删除结点的操作易于实现。拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况。拉链法解决冲突时,需要使用指针,指示下一个元素的存储位置
    5. 开哈希表–链式地址法;闭哈希表–开放地址法.开哈希和闭哈希主要的区别在于,随着哈希表的密集度提高,使用闭哈希时,不仅会与相同哈希值的元素发生冲突,还容易与不同哈希值的元素发生冲突;而开哈希则不受哈希表疏密与否的影响,始终只会与相同哈希值的元素冲突而已。所以在密集度变大的哈希表中查找时,显然开哈希的平均搜索长度不会增长。
    6. 设有n个关键字具有相同的Hash函数值,则用线性探测法把这n个关键字映射到Hash表中需要做n*(n-1)/2次线性探测。如果使用二次探测再散列法将这n个关键字存入哈希表,至少要进行n*(n+1)/2次探测

    Hash查找效率:装填因子=表中记录数/表容量

    有B+Tree/Hash_Map/STL Map三种数据结构。对于内存中数据,查找性能较好的数据结构是Hash_Map,对于磁盘中数据,查找性能较好的数据结构是B+Tree。Hash操作能根据散列值直接定位数据的存储地址,设计良好的hash表能在常数级时间下找到需要的数据,但是更适合于内存中的查找。B+树是一种是一种树状的数据结构,适合做索引,对磁盘数据来说,索引查找是比较高效的。STL_Map的内部实现是一颗红黑树,但是只是一颗在内存中建立二叉树树,不能用于磁盘操作,而其内存查找性能也比不上Hash查找。

    内部排序

    1. 内部排序:全部数据可同时放入内存进行的排序。
    2. 外部排序:文件中数据太多,无法全部调入内存进行的排序。

    插入类:

    1. 直接插入排序。最坏情况是数据递减序,数据比较和移动量最大,达到O(n2),最好是数据是递增序,比较和移动最少为O(n)。趟数是固定的n-1,即使有序,也要依次从第二个元素开始。排序趟数不等于时间复杂度。
    2. 折半插入排序 。由于插入第i个元素到r[1]到r[i-1]之间时,前i个数据是有序的,所以可以用折半查找确定插入位置,然后插入。
    3. 希尔排序。缩小增量排序。5-3-1。在实际应用中,步长的选取可简化为开始为表长n的一半(n/2),以后每次减半,最后为1。插入的改进,最后一趟已基本有序,比较次数和移动次数相比直接插入最后一趟更少

    交换类:

    1. 冒泡排序。O(n2)通常认为冒泡是比较差的,可以加些改进,比如在一趟中无数据的交换,则结束等措施。
      • 在数据已基本有序时,冒泡是一个较好的方法
      • 在数据量较少时(15个左右)可以用冒泡
    2. 快速排序。
      • 时间复杂度。最好情况:每次支点总在中间,O(nlog2n),平均O(nlog2n)。最坏,数据已是递增或递减,O(n2)。pivotkey的选择越靠近中央,即左右两个子序列长度越接近,排序速度越快。越无序越快。
      • 空间复杂度。需栈空间以实现递归,最坏情况:S(n)=O(n);一般情况:S(n)=O(log2n)
      • 在序列已是有序的情况下,时间复杂度最高。原因:支点选择不当。改进:随机选取支点或最左、最右、中间三个元素中的值处于中间的作为支点,通常可以避免最坏情况。所以,快速排序在表已基本有序的情况下不合适。
      • 在序列长度已较短时,采用直接插入排序、起泡排序等排序方法。序列的个数通常取10左右。

    选择类排序:

    1. 简单选择排序。O(n2)。总比较次数n(n-1)/2。
    2. 堆排序。建堆 O(n),筛选排序O(nlogn)。找出若干个数中最大/最小的前K个数,用堆排序是最好。小根堆中最大的数一定是放在叶子节点上,堆本身是个完全二叉树,完全二叉树的叶子节点的位置大于[n/2]。时间复杂度不会因为待排序序列的有序程度而改变,但是待排序序列的有序程度会影响比较次数。
    3. 归并排序。时间:与表长成正比,若一个表表长是m,另一个是n,则时间是O(m+n)。单独一个数组归并,时间:O(nlogn),空间:O(n),比较次数介于(nlogn)/2和(nlogn)-n+1,赋值操作的次数是(2nlogn)。归并排序算法比较占用内存,但却是效率高且稳定的排序算法。在外排序中使用。归并的趟数是logn。
    4. 基数排序。在一般情况下,每个结点有 d 位关键字,必须执行 t = d次分配和收集操作。分配的代价:O(n);收集的代价:O(rd) (rd是基数);总的代价为:O( d ×(n + rd))。适用于以数字和字符串为关键字的情况。
    5. 枚举排序,通常也被叫做秩排序,比较计数排序。对每一个要排序的元素,统计小于它的所有元素的个数,从而得到该元素在整个序列中的位置,时间复杂度为O(n2)

    比较法分类的下界:O(nlogn)

    排序算法的一些特点:

    1. 堆排序、冒泡排序、快速排序在每趟排序过程中,都会有一个元素被放置在其最终的位置上。
    2. 有字符序列 {Q,H,C,Y,P,A,M,S,R,D,F,X} ,新序列{F,H,C,D,P,A,M,Q,R,S,Y,X},是快速排序算法一趟扫描的结果。(拿Q作为分割点,快速排序一轮。二路归并,第一趟排序,得到 n / 2 个长度为 2 的各自有序的子序列,第二趟排序,得到 n / 4 个长度为 4 的各自有序的子序列H Q C Y A P M S D R F X。如果是快速排序的话,第一个元素t将会被放到一个最准确的位置,t前的数均小于t,后面的数均大于t。希尔排序每个小分组内将会是有序的。堆排序,把它构成一颗二叉树的时候,该堆要么就是大根堆,要么就是小根堆,第一趟Y排在最后;冒泡,那么肯定会有数据下沉的动作,第一趟有A在第一位。)
    3. 在文件”局部有序”或文件长度较小的情况下,最佳内部排序的方法是直接插入排序。(归并排序要求待排序列已经部分有序,而部分有序的含义是待排序列由若干有序的子序列组成,即每个子序列必须有序,并且其时间复杂度为O(nlog2n);直接插入排序在待排序列基本有序时,每趟的比较次数大为降低,即n-1趟比较的时间复杂度由O(n^2)降至O(n)。在待排序的元素序列基本有序或者每个元素距其最终位置不远也可用插入排序,效率最高的排序方法是插入排序
    4. 排序趟数与序列的原始状态有关的排序方法是优化冒泡和快速排序法。(插入排序和选择排序不管序列的原始状态是什么都要执行n-1趟,优化冒泡和快排不一定。仔细理解排序的次数比较次数的区别)
    5. 不稳定的排序方法:快排,堆排,希尔,选择
    6. 要与关键字的初始排列次序无关,那么就是最好、最坏、一般的情况下排序时间复杂度不变, 总共有堆排序,归并排序,选择排序,基数排序
    7. 快速排序、Shell 排序、归并排序、直接插入排序的关键码比较次数与记录的初始排列有关。折半插入排序、选择排序无关。(直接插入排序在完全有序的情况下每个元素只需要与他左边的元素比较一次就可以确定他最终的位置;折半插入排序,比较次数是固定的,与初始排序无关;快速排序,初始排序不影响每次划分时的比较次数,都要比较n次,但是初始排序会影响划分次数,所以会影响总的比较次数,但快排平均比较次数最小;归并排序在归并的时候,如果右路最小值比左路最大值还大,那么只需要比较n次,如果右路每个元素分别比左路对应位置的元素大,那么需要比较2*n-1次,所以与初始排序有关)
    8. 精俭排序,即一对数字不进行两次和两次以上的比较,插入和归并是“精俭排序”。插入排序,前面是有序的,后面的每一个元素与前面有序的元素比较,比较过的就是有序的了,不会再比较一次。归并每次合并后,内部都是有序的,内部的元素之间不用再比较。选择排序,每次在后面的元素中找到最小的,找最小元素的过程是在没有排好序的那部分进行,所有肯定会比较多次。堆排序也需比较多次。

    外部排序

    1. 生成合并段(run):读入文件的部分记录到内存->在内存中进行内部排序->将排好序的这些记录写入外存,形成合并段->再读入该文件的下面的记录,往复进行,直至文件中的记录全部形成合并段为止。
    2. 外部合并:将上一阶段生成的合并段调入内存,进行合并,直至最后形成一个有序的文件。
    3. 外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。外部排序最常用的算法是多路归并排序,即将原文件分解成多个能够一次性装入内存的部分,分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行多路归并排序
    4. 不管初始序列是否有序, 冒泡、选择排序时间复杂度是O(n^2),归并、堆排序时间复杂度是O(nlogn)
    5. 外部排序的总时间 = 内部排序(产出初始归并段)所需时间 + 外存信息读取时间 + 内部归并所需的时间
    6. 外排中使用置换选择排序的目的,是为了增加初始归并段的长度。减少外存读写次数需要减小归并趟数

    7. 根据内存容量设若干个输入缓冲区和一个输出缓冲区。若采用二路归并,用两个输入缓冲。

    8. 归并的方法类似于归并排序的归并算法。增加的是对缓冲的监视,对于输入,一旦缓冲空,要到相应文件读后续数据,对于输出缓冲,一旦缓冲满,要将缓冲内容写到文件中去。
    9. 外排序和内排序不只是考虑内外排序算法的性能,还要考虑IO数据交换效率的问题,内存存取速度远远高于外存。影响外排序的时间因素主要是内存与外设交换信息的总次数

    有效的算法设计

    1. 贪心法。Dijkstra的最短路径(时间复杂度O(n2));Prim求最小生成树邻接表存储时是O(n+e),图O(n2);关键路径及关键活动的求法。
    2. 回溯法
    3. 分支限界法
    4. 分治法。分割、求解、合并。二分查找、归并排序、快速排序。
    5. 动态规划。Floyd-Warshall算法求解图中所有点对之间最短路径时间复杂度为O(n3)

    动态规划解题的方法是一种高效率的方法,其时间复杂度通常为O(n2),O(n3)等,可以解决相当大的信息量。(数塔在n<=100层时,可以在很短的时间内得到问题解)

    • 适用的原则:原则为优化原则,即整体优化可以分解为若干个局部优化。
    • 动态规划比穷举法具有较少的计算次数
    • 递归算法需要很大的栈空间,而动态规划不需要栈空间

    贪心和动态规划的差别:

    1. 所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
    2. 在动态规划算法中,每步所作的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能作出选择。而在贪心算法中,仅在当前状态下作出最好选择,即局部最优选择。然后再去解作出这个选择后产生的相应的子问题。
    3. 贪心算法所作的贪心选择可以依赖于以往所作过的选择,但决不依赖于将来所作的选择,也不依赖于子问题的解。正是由于这种差别,动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为一个规模更小的子问题。

    P问题

    1. P问题,如果它可以通过运行多项式次(即运行时间至多是输入量大小的多项式函数的一种算法获得解决),可以找到一个能在多项式的时间里解决它的算法。—-确定性问题
    2. NP问题,虽然可以用计算机求解,但是对于任意常数k,它们不能在O(nk)时间内得到解答,可以在多项式的时间里验证一个解的问题。所有的P类问题都是NP问题。
    3. NP完全问题,知道有效的非确定性算法,但是不知道是否存在有效的确定性算法,同时,不能证明这些问题中的任何一个不存在有效的确定性算法。这类问题称为NP完全问题。
    展开全文
  • 数据结构知识整理

    万次阅读 多人点赞 2018-07-30 18:50:47
    基于严蔚敏及吴伟民编著的清华大学C语言版教材并结合网上相关资料整理(http://www.docin.com/p-2027739005.html) ...数据:对客观事物的符号表示,在计算机科学中是指所有输入到计算机中并被计算...

    基于严蔚敏及吴伟民编著的清华大学C语言版教材并结合网上相关资料整理(http://www.docin.com/p-2027739005.html)

    第一章:绪论

    1.数据结构:是一门研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。

    2.数据结构涵盖的内容:

    3.基本概念和术语:

    数据:对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。

    数据元素:数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。

    数据对象:性质相同的数据元素的集合,是数据的一个子集。

    数据结构:相互之间存在一种或多种特定关系的数据元素的集合。

    数据类型:一个值的集合和定义在这个值集上的一组操作的总称。

    4.算法和算法分析

    1)算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作。

    算法五个特性:有穷性,确定性,可行性,输入,输出。

    2)算法设计要求:正确性,可读性,健壮性,效率与低存储量需求。

    3)算法分析:时间复杂度,空间复杂度,稳定性

     

    第二章:线性表

    1.线性结构特点:在数据元素的非空有限集合中,(1)存在唯一的一个被称做“第一个”的数据元素;(2)存在唯一的一个被称做“最后一个”的数据元素;(3)除第一个之外,集合中的每个数据元素均只有一个前驱;(4)除最后一个之外,集合中每个数据元素均只有一个后继。

    2.线性表定义:有限个性质相同的数据元素组成的序列。

    3.线性表的存储结构:顺序存储结构和链式存储结构

    顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。

    通常用数组来描述数据结构中的顺序存储结构。

    链式存储结构: 其结点在存储器中的位置是随意的,即逻辑上相邻的数据元素在物理上不一定相邻。通过指针来实现。

    数据结构的基本运算:修改、插入、删除、查找、排序

    4.线性表的顺序表示和实现

    1)修改:通过数组的下标便可访问某个特定元素并修改。

    时间复杂度O(1)

    2) 插入:在线性表的第i个位置前插入一个元素

    实现步骤:

       ①将第n至第i 位的元素逐一向后移动一个位置;

       ②将要插入的元素写到第i个位置;

       ③表长加1。

       注意:事先应判断: 插入位置i 是否合法?表是否已满?

       应当符合条件: 1≤i≤n+1  或  i=[1, n+1]

       核心语句:

    for (j=n; j>=i; j--)

    a[j+1]=a[ j ]; 

    a[ i ]=x;   

    n++;

      插入时的平均移动次数为:n(n+1)/2÷(n+1)=n/2≈O(n)

    3)删除:删除线性表的第i个位置上的元素

      实现步骤:

      ①将第i+1 至第n 位的元素向前移动一个位置;

      ②表长减1。

      注意:事先需要判断,删除位置i 是否合法?

      应当符合条件:1≤i≤n  或  i=[1, n]

      核心语句:

    {

        for ( j=i+1; j<=n; j++ )

        a[j-1]=a[j]; 

        n--;

    }

      顺序表删除一元素的时间效率为:T(n)=(n-1)/2 O(n)

      顺序表插入、删除算法的平均空间复杂度为O(1)

    5.线性表的链式表示和实现

    线性链表:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。

    一个数据元素称为一个结点,包括两个域:存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。

    由于链表的每个结点中只包含一个指针域,故线性链表又称为单链表。

    1)单链表的修改(或读取)

    思路:要修改第i个数据元素,必须从头指针起一直找到该结点的指针preturn p;  

    然后才能:p->data=new_value

    读取第i个数据元素的核心语句是:

    Linklist *find(Linklist *head ,int i)

    { 

       int j=1;

       Linklist *p;

       P=head->next;

       While((p!=NULL)&&(j<i))

       {

           p=p->next;

           j++;

        }

    }

    2)单链表的插入

    链表插入的核心语句:

    Step 1:s->next=p->next;

    Step 2:p->next=s;

    3)单链表的删除

    删除动作的核心语句(要借助辅助指针变量q):

    q = p->next;           //首先保存b的指针,靠它才能找到c;

    p->next=q->next;  //将a、c两结点相连,淘汰b结点;

    free(q) ;               //彻底释放b结点空间

    4)双向链表的插入操作

    设p已指向第i 元素,请在第 i 元素前插入元素 x:

    ① ai-1的后继从 ai ( 指针是p)变为 x(指针是s) :

                    s->next = p  ;   p->prior->next = s ;

    ② ai  的前驱从ai-1 ( 指针是p->prior)变为 x ( 指针是s);

                    s->prior = p ->prior ; p->prior = s ;

    5)双向链表的删除操作

    设p指向第i 个元素,删除第 i 个 元素

    后继方向:ai-1的后继由ai ( 指针p)变为ai+1(指针 p ->next );

                       p ->prior->next =  p->next  ;

    前驱方向:ai+1的前驱由ai ( 指针p)变为ai-1 (指针 p -> prior );

                     p->next->prior = p ->prior ;

    6.循环链表

    循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。

    循环链表的操作和线性链表基本一致,差别仅在于算法中的循环条件不是p或p->next是否为空,而是它们是否等于头指针。

    学习重点:

    • 线性表的逻辑结构,指线性表的数据元素间存在着线性关系。在顺序存储结构中,元素存储的先后位置反映出这种线性关系,而在链式存储结构中,是靠指针来反映这种关系的。

    • 顺序存储结构用一维数组表示,给定下标,可以存取相应元素,属于随机存取的存储结构。

    • 链表操作中应注意不要使链意外“断开”。因此,若在某结点前插入一个元素,或删除某元素,必须知道该元素的前驱结点的指针。

    • 掌握通过画出结点图来进行链表(单链表、循环链表等)的生成、插入、删除、遍历等操作。

    • 数组(主要是二维)在以行序/列序为主的存储中的地址计算方法。

    补充重点:

    • 每个存储结点都包含两部分:数据域和指针域(链域)

    • 在单链表中,除了首元结点外,任一结点的存储位置由 其直接前驱结点的链域的值 指示。

    • 在链表中设置头结点有什么好处?

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

    • 如何表示空表?

    (1)无头结点时,当头指针的值为空时表示空表;

    (2)有头结点时,当头结点的指针域为空时表示空表。

    • 链表的数据元素有两个域,不再是简单数据类型,编程时该如何表示?

     因每个结点至少有两个分量,且数据类型通常不一致,所以要采用结构数据类型。

    • sizeof(x)——  计算变量x的长度(字节数);

              malloc(m) —  开辟m字节长度的地址空间,并返回这段空间的首地址;

              free(p)   —— 释放指针p所指变量的存储空间,即彻底删除一个变量。

    • 链表的运算效率分析:

    (1)查找 

    因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)。

    (2) 插入和删除 

    因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。

    但是,如果要在单链表中进行前插或删除操作,因为要从头查找前驱结点,所耗时间复杂度将是 O(n)。

    例:在n个结点的单链表中要删除已知结点*P,需找到它的前驱结点的地址,其时间复杂度为 O(n)

    • 顺序存储和链式存储的区别和优缺点?

        顺序存储时,逻辑上相邻的数据元素,其物理存放地址也相邻。顺序存储的优点是存储密度大,存储空间利用率高;缺点是插入或删除元素时不方便。

      链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针。链式存储的优点是插入或删除元素时很方便,使用灵活。缺点是存储密度小,存储空间利用率低。

    • 顺序表适宜于做查找这样的静态操作;

    • 链表宜于做插入、删除这样的动态操作。

    • 若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;

    • 若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。

    ① 数组中各元素具有统一的类型;

    ② 数组元素的下标一般具有固定的上界和下界,即数组一旦被定义,它的维数和维界就不再改变。

    ③数组的基本操作比较简单,除了结构的初始化和销毁之外,只有存取元素和修改元素值的操作。

    • 三元素组表中的每个结点对应于稀疏矩阵的一个非零元素,它包含有三个数据项,分别表示该元素的 行下标  、列下标 和 元素值 。

    第三章:栈和队列

    1.栈:限定仅在表尾进行插入或删除操作的线性表。

    栈的基本操作:在栈顶进行插入或删除,栈的初始化、判空及取栈顶元素等。

    入栈口诀:堆栈指针top “先压后加”

    出栈口诀:堆栈指针top “先减后弹”

    top=0表示空栈。

     

    2.栈的表示和实现

         1)构造一个空栈S

        Status InitStack(SqStack &S)

        {

            S.base = (SElemType *) malloc(STACK_INIT_SIZE * sizeof(SElemType));

            if(!S.base) exit (OVERFLOW); //存储分配失败

                S.top = S.base;

                S.stacksize = STACK_INIT_SIZE;

                return OK;

        }

     

        2)返回栈顶元素

           Status GetTop(SqStack S, SElemType e) 

            {//若栈不空,则用e返回S的栈顶元素,并返回OK,否则返回ERROR

                if(S.top == S.base) return ERROR;

                e = *(S.top-1);

                return OK; 

            }//GetTop

     

        3)顺序栈入栈函数PUSH()

    Status Push(SqStack &S, SElemType e)

    { //插入元素e为新的栈顶元素

        if(S.top-S.base>=S.stacksize)//栈满,追加存储空间

       {

        s.base = (SElemType*)realloc(S.base,(S.stacksize+STACKINCREMENT)*sizeof(SElemType));

        if(!S.base) exit(OVERFLOW);//存储分配失败

        S.top = S.base + S.stacksize;

        S.stacksize += STACKINCREMENT;

        }

        *S.top++ =e;

        return OK:

    }//PUSH

    4)顺序栈出栈函数POP()

    status Pop( SqStack &S,SElemType &e)

    { //若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR

        if(S.top == S.base) return ERROR; 

        e=* —S.top;

        return OK;

            }

    3.栈的应用

    数制转换,括号匹配的检验,行编辑程序,迷宫求解,表达式求值,递归实现。

    4.队列:是一种先进先出的线性表,它只允许在表的一端进行插入,而在另一端删除元素。

    允许插入的一端叫做队尾,允许删除的一端叫做队头。

    除了栈和队列外,还有一种限定性数据结构是双端队列。双端队列是限定插入和删除操作在表的两端进行的线性表。

    5.链队列结点类型定义:

     typedef Struct QNode{

         QElemType        data;     //元素

         Struct   QNode   *next;  //指向下一结点的指针

       }Qnode , * QueuePtr ;

    链队列类型定义:

     typedef  struct {

      QueuePtr     front ; //队首指针

      QueuePtr     rear ; //队尾指针

      }  LinkQueue;

    链队示意图:

    ①  空链队的特征:front=rear

    ②  链队会满吗?一般不会,因为删除时有free动作。除非内存不足!

    ③  入队(尾部插入):rear->next=S; rear=S;

        出队(头部删除):front->next=p->next;

    6.顺序队

    顺序队类型定义:

    #define    QUEUE-MAXSIZE  100  //最大队列长度

         typedef    struct {

                QElemType     *base;    //队列的基址

                 int                    front;     //队首指针

                 int                     rear;    //队尾指针

           }SqQueue

    建队核心语句:

    q.base=(QElemType *)malloc(sizeof (QElemType)* QUEUE_MAXSIZE);       //分配空间

    顺序队示意图:

    7.循环队列:

    队空条件 :  front = rear       (初始化时:front = rear )

    队满条件: front = (rear+1) % N         (N=maxsize)

    队列长度(即数据元素个数):L=(N+rear-front)% N

    1)初始化一个空队列

    Status   InitQueue ( SqQueue  &q ) //初始化空循环队列 q

    {   

    q.base=(QElemType *)malloc(sizeof(QElemType)* QUEUE_MAXSIZE);   //分配空间

    if (!q.base)  exit(OVERFLOW);//内存分配失败,退出程序

       q.front =q.rear=0; //置空队列

        return OK;

     } //InitQueue;

     

    2)入队操作

    Status   EnQueue(SqQueue  &q,  QElemType e)

    {//向循环队列 q 的队尾加入一个元素 e

       if ( (q.rear+1) %  QUEUE_MAXSIZE ==  q.front)

                        return  ERROR ; //队满则上溢,无法再入队

            q.rear = ( q.rear + 1 ) %  QUEUE_MAXSIZE;

            q.base [q.rear] = e;    //新元素e入队

            return  OK;

     }// EnQueue;

     

    3)出队操作

    Status     DeQueue ( SqQueue  &q,    QElemType &e)

     {//若队列不空,删除循环队列q的队头元素,

            //由 e 返回其值,并返回OK

          if ( q.front = = q.rear )   return ERROR;//队列空

          q.front=(q.front+1) % QUEUE_MAXSIZE; 

          e = q.base [ q.front ] ;

         return OK;

        }// DeQueue

    • 链队列空的条件是首尾指针相等,而循环队列满的条件的判定,则有队尾加1等于队头和设标记两种方法。

    补充重点:

    1. 为什么要设计堆栈?它有什么独特用途?

    2. 什么叫“假溢出” ?如何解决?

     调用函数或子程序非它莫属;

     递归运算的有力工具;

     用于保护现场和恢复现场;

     简化了程序设计的问题。                         

    2.为什么要设计队列?它有什么独特用途?

     离散事件的模拟(模拟事件发生的先后顺序,例如 CPU芯片中的指令译码队列);

     操作系统中的作业调度(一个CPU执行多个作业);

     简化程序设计。

    答:在顺序队中,当尾指针已经到了数组的上界,不能再有入队操作,但其实数组中还有空位置,这就叫“假溢出”。解决假溢出的途径———采用循环队列。

    4.在一个循环队列中,若约定队首指针指向队首元素的前一个位置。那么,从循环队列中删除一个元素时,其操作是先 移动队首位置 ,后 取出元素

    5.线性表、栈、队的异同点:

    相同点:逻辑结构相同,都是线性的;都可以用顺序存储或链表存储;栈和队列是两种特殊的线性表,即受限的线性表(只是对插入、删除运算加以限制)。

    不同点:① 运算规则不同:

    线性表为随机存取;

    而栈是只允许在一端进行插入和删除运算,因而是后进先出表LIFO

    队列是只允许在一端进行插入、另一端进行删除运算,因而是先进先出表FIFO

     用途不同,线性表比较通用;堆栈用于函数调用、递归和简化设计等;队列用于离散事件模拟、OS作业调度和简化设计等。

     

    第四章:串

    1.串是数据元素为字符的线性表,串的定义及操作。

    串即字符串,是由零个或多个字符组成的有限序列,是数据元素为单个字符的特殊线性表。

    串比较:int strcmp(char *s1,char *s2);            

    求串长:int strlen(char *s);                          

    串连接:char  strcat(char *to,char *from)   

    子串T定位:char strchr(char *s,char *c);

    2.串的存储结构,因串是数据元素为字符的线性表,所以存在“结点大小”的问题。

    模式匹配算法 

    串有三种机内表示方法:

     

    3.模式匹配算法 :

    算法目的:确定主串中所含子串第一次出现的位置(定位)

    定位问题称为串的模式匹配,典型函数为Index(S,T,pos)

    BF算法的实现—即编写Index(S, T, pos)函数

    BF算法设计思想:

    将主串S的第pos个字符和模式T的第1个字符比较,

    若相等,继续逐个比较后续字符;

    若不等,从主串S的下一字符(pos+1)起,重新与T第一个字符比较。 

    直到主串S的一个连续子串字符序列与模式T相等。返回值为S中与T匹配的子序列第一个字符的序号,即匹配成功。

    否则,匹配失败,返回值 0。

    Int Index_BP(SString S, SString T, int pos)

    { //返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数值为0.

     // 其中,T非空,1≤pos≤StrLength(S)

        i=pos;      j=1;

       while ( i<=S[0] && j<=T[0] ) //如果i,j二指针在正常长度范围,

         {  

           if (S[i] = = T[j] ) {++i, ++j; }   //则继续比较后续字符

           else {i=i-j+2; j=1;} //若不相等,指针后退重新开始匹配

          }

      if(j>T[0]) return i-T[0];  //T子串指针j正常到尾,说明匹配成功,  else return 0;        //否则属于i>S[0]情况,i先到尾就不正常

    } //Index_BP

    补充重点:

    1.空串和空白串有无区别?

    答:有区别。

    空串(Null String)是指长度为零的串;

    而空白串(Blank  String),是指包含一个或多个空白字符‘  ’(空格键)的字符串.

    2.“空串是任意串的子串;任意串S都是S本身的子串,除S本身外,S的其他子串称为S的真子串。”

     

    第五章:数组和广义表

    重点:二维数组的位置计算。

    矩阵的压缩存储:特殊矩阵(三角矩阵,对称矩阵),稀疏矩阵,

     

    第六章:树和二叉树

    1.树:是n(n≥0)个结点的有限集。(1)有且仅有一个特定的称为根(root)的结点;(2)当n>1时,其余的结点分为m(m≥0)个互不相交的有限集合T1,T2,…,Tm。每个集合本身又是棵树,被称作这个根的子树 。

    2.二叉树:是n(n≥0)个结点的有限集合,由一个根结点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。

    二叉树的性质,存储结构。

    性质1: 在二叉树的第i层上至多有2^(i-1)个结点(i>0)。

    性质2: 深度为k的二叉树至多有2^k-1个结点(k>0)。

    性质3: 对于任何一棵二叉树,如果其终端结点数为n0,度为2的结点数有n2个,则叶子数n0=n2+1

    性质4: 具有n个结点的完全二叉树的深度必为 [log2n]+1

    性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号为2i+1;其双亲的编号必为i/2(i=1 时为根,除外)。

    二叉树的存储结构:

    1).顺序存储结构

    按二叉树的结点“自上而下、从左至右”编号,用一组连续的存储单元存储。

    若是完全/满二叉树则可以做到唯一复原。

    不是完全二叉树:一律转为完全二叉树!

    方法很简单,将各层空缺处统统补上“虚结点”,其内容为空。

    缺点:①浪费空间;②插入、删除不便 

    2).链式存储结构

    用二叉链表即可方便表示。一般从根结点开始存储。

    lchild

    data

    rchild

    优点:①不浪费空间;②插入、删除方便

    3.二叉树的遍历。

    指按照某种次序访问二叉树的所有结点,并且每个结点仅访问一次,得到一个线性序列。

    遍历规则:二叉树由根、左子树、右子树构成,定义为D、 L、R

    若限定先左后右,则有三种实现方案:

      DLR               LDR                   LRD

    先序遍历           中序遍历          后序遍历

    4.线索二叉树

    1)线索二叉树可以加快查找前驱与后继结点,实质就是将二叉链表中的空指针改为指向前驱或后继的线索,线索化就是在遍历中修改空指针。

    通常规定:对某一结点,若无左子树,将lchild指向前驱结点;若无右子树,将rchild指向后继结点。

    还需要设置左右两个tag,用来标记当前结点是否有子树。

    若ltag==1,lchild指向结点前驱;若rtag==1,rchild指向结点后继。

    2)线索二叉树的存储结构如下:

    typedef struct ThreadNode{

    ElemType data;

    struct ThreadNode *lchild, *rchild;

    int ltag, rtag;

    }ThreadNode, *ThreadTree;

    5.树和森林

    1)树有三种常用存储方式:

    ①双亲表示法     ②孩子表示法    ③孩子—兄弟表示法

    2)森林、树、二叉树的转换

    (1)将树转换为二叉树

    树中每个结点最多只有一个最左边的孩子(长子)和一个右邻的兄弟。按照这种关系很自然地就能将树转换成相应的二叉树:a.在所有兄弟结点之间加一连线

    b.对每个结点,除了保留与其长子的连线外,去掉该结点与其它孩子的连线。

    (2)将一个森林转换为二叉树:

    具体方法是:a.将森林中的每棵树变为二叉树;

    b.因为转换所得的二叉树的根结点的右子树均为空,故可将各二叉树的根结点视为兄弟从左至右连在一起,就形成了一棵二叉树。

    (3)二叉树转换为树

    是树转换为二叉树的逆过程。

    a.加线。若某结点X的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点,都作为结点X的孩子。将结点X与这些右孩子结点用线连接起来。

    b.去线。删除原二叉树中所有结点与其右孩子结点的连线。

     

    (4)二叉树转换为森林:

    假如一棵二叉树的根节点有右孩子,则这棵二叉树能够转换为森林,否则将转换为一棵树。

    a.从根节点开始,若右孩子存在,则把与右孩子结点的连线删除。再查看分离后的二叉树,若其根节点的右孩子存在,则连线删除。直到所有这些根节点与右孩子的连线都删除为止。

    b.将每棵分离后的二叉树转换为树。

    6.树和森林的遍历

    • 树的遍历

    ① 先根遍历:访问根结点;依次先根遍历根结点的每棵子树。

    ② 后根遍历:依次后根遍历根结点的每棵子树;访问根结点。

     

    • 森林的遍历

    ① 先序遍历

    若森林为空,返回;

    访问森林中第一棵树的根结点;

    先根遍历第一棵树的根结点的子树森林;

    先根遍历除去第一棵树之后剩余的树构成的森林。

    ② 中序遍历

    若森林为空,返回;

    中根遍历森林中第一棵树的根结点的子树森林;

    访问第一棵树的根结点;

    中根遍历除去第一棵树之后剩余的树构成的森林。

     

    7.哈夫曼树及其应用

    Huffman树:最优二叉树(带权路径长度最短的树)

    Huffman编码:不等长编码。

    树的带权路径长度:(树中所有叶子结点的带权路径长度之和)

    构造Huffman树的基本思想:权值大的结点用短路径,权值小的结点用长路径。

    构造Huffman树的步骤(即Huffman算法):

    (1) 由给定的 n 个权值{ w1, w2, …, wn }构成n棵二叉树的集合F = { T1, T2, …, Tn } (即森林) ,其中每棵二叉树 Ti 中只有一个带权为 wi 的根结点,其左右子树均空。

    (2) 在F 中选取两棵根结点权值最小的树 做为左右子树构造一棵新的二叉树,且让新二叉树根结点的权值等于其左右子树的根结点权值之和。

    (3) 在F 中删去这两棵树,同时将新得到的二叉树加入 F中。

    (4) 重复(2) 和(3) , 直到 F 只含一棵树为止。这棵树便是Huffman树。

    具体操作步骤:

    应用:用于通信编码

      在通信及数据传输中多采用二进制编码。为了使电文尽可能的缩短,可以对电文中每个字符出现的次数进行统计。设法让出现次数多的字符的二进制码短些,而让那些很少出现的字符的二进制码长一些。假设有一段电文,其中用到 4 个不同字符A,C,S,T,它们在电文中出现的次数分别为 7 , 2 , 4 , 5 。把 7 , 2 , 4 , 5 当做 4 个叶子的权值构造哈夫曼树如图(a) 所示。在树中令所有左分支取编码为 0 ,令所有右分支取编码为1。将从根结点起到某个叶子结点路径上的各左、右分支的编码顺序排列,就得这个叶子结点所代表的字符的二进制编码,如图(b) 所示。这些编码拼成的电文不会混淆,因为每个字符的编码均不是其他编码的前缀,这种编码称做前缀编码。

     

    第七章 图

    1.图的定义,概念、术语及基本操作(https://blog.csdn.net/eyishion/article/details/53234255)

    1)图的定义

    图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成;

    通常表示为:G(V,E),G表示一个图,V是图G中顶点的集合,E是图G中边的集合;

    注意:在图中数据元素称之为顶点(Vertex),而且顶点集合有穷非空;在图中任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示。

    2)图的分类

    • 按照有无方向,分为无向图有向图

    无向图:如果图中任意两个顶点之间的边都是无向边,则称该图为无向图。

    无向边:若顶点M到顶点N的边没有方向,称这条边为无向边,用无序偶对(M,N)或(N,M)表示。

    无向图是有顶点构成。如下图所示就是一个无向图G1:

    无向图G1= (V1,{E1}),其中顶点集合 V1={A,B,C,D};边集合E1={(A,B),(B,C),(C,D),(D,A)}

    有向图:如果图中任意两个顶点之间的边都是有向边,则称该图为有向图。

    有向边:若顶点M到顶点N的边有方向,称这条边为有向边,也称为弧,用偶序对 < M, N >表示;M表示弧尾,N表示弧头

    有向图是有顶点构成,如下图所示是一个有向图G2:

    有向图G2=(V2,{E2}),其中顶点集合 V2={A,B,C,D};弧集合E2={< A,D>,< B,A>,< C,A>,< B,C>}

    对于弧< A,D>来说, A是弧尾,D是弧头

    注意:无向边用 小括号 “()”表示,有向边用“<>”表示。

    无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。

    含有n个顶点的无向完全图有n * (n-1)/2条边。下面是一个无向完全图

    4个顶点,6条无向边,每个顶点对应3条边 ,一共4个顶点 总共 4*3,每个顶点对应的边都重复计算了一次,所以整体要除以2。

    对于n各 顶点和e条边的无向图满足:0<=e <= n(n-1)/2

    有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。

    含有n个顶点的无向完全图有n * (n-1)条边。下面是一个有向完全图

    4个顶点,12条弧,一共4个顶点 总共 4*3。

    2,按照弧或边的多少,分为稀疏图稠密图

    若边或弧的个数e<=NlogN(N是顶点的个数),称为系数图,否则称为稠密图;

    3,按照边或弧是否带权,其中带权的图统称为

    有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权。

    有向网中弧带权的图叫做有向网;

    无向网中边带权的图叫做无向网;

    比如下图就是一个无向图

     

    图的顶点和边间关系

    邻接点 度 入度 出度

    对于无向图,假若顶点v和顶点w之间存在一条边,则称顶点v和顶点w互为邻接点,边(v,w)和顶点v和w相关联。

    顶点v的是和v相关联的边的数目,记为TD(v);

    上面这个无向图G1,A和B互为邻接点,A和D互为邻接点,B和C互为邻接点,C和D互为邻接点;

    A的度是2,B的度是2,C的度是2,D的度是2;所有顶点度的和为8,而边的数目是4;

    图中边的数目e = 各个顶点度数和的一半。

    对于有向图来说,与某个顶点相关联的弧的数目称为度(TD);以某个顶点v为弧尾的弧的数目定义为顶点v的出度(OD);以顶点v为弧头的弧的数目定义为顶点的入度(ID)

    度(TD) = 出度(OD) + 入度(ID);

    比如上面有向图,

    A的度为3 ,A的入度 2,A的出度是1

    B的度为2 ,B的入度 0,B的出度是2

    C的度为2 ,C的入度 1,C的出度是1

    D的度为1 ,D的入度 1,D的出度是0

    所有顶点的入度和是4,出度和也是4,而这个图有4个弧

    所以 有向图的弧 e = 所有顶点入度的和 = 所有顶点出度的和

    路径 路径长度 简单路径 回路 (环) 简单回路(简单环)

    设图G=(V,{E})中的一个顶点序列{u=Fi0,Fi1,Fi2,….Fim=w}中,(Fi,j-1,Fi,j)∈E 1 ≤j ≤m,则称从顶点u到顶点w之间存在一条路径,路径上边或弧的数目称作路径长度

    若路径中的顶点不重复出现的路径称为简单路径

    若路径中第一个顶点到最后一个顶点相同的路径称为回路或环

    若路径中第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环

    比如下图 :

    从B 到 D 中顶点没有重复出现 ,所以是简单路径 ,边的数目是2,所以路径长度为 2。

    图1和图2都是一个回路(环),图1中出了第一个顶点和最后一个顶点相同之外,其余顶点不相同,所以是简单环(简单回路),图2,有与顶点C重复就不是一个简单环了;

    连通图相关术语

    连通图

    在无向图G(V,{E})中,如果从顶点V到顶点W有路径,则称V和W是连通的。如果对于图中任意两个顶点Vi、Vj∈V,Vi和Vj都是连通的,则称G是连通图。

    如下图所示:

    图1,顶点A到顶点E就无法连通,所以图1不是连通;图2,图3,图4属于连通图;

    连通分量

    若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量;

    图1是无向非连通图,由两个连通分量,分别是图2和图3。图4尽管也是图1的子图,但是它不满足极大连通,也就说极大连通应当是包含ABCD四个顶点,比如图2那样;

    强连通图

    在有向图G(V,{E})中,如果对于每一对Vi ,Vj∈V,Vi≠Vj,从Vi到Vj和从Vj到Vi都存在有向路径,则称G是强连通图。

    图1不是强连通图因为D到A不存在路径,图2属于强连通图。

    强连通分量

    若有向图不是强连通图,则图中各个极大强连通子图称作此图的强连通分量;

    图1不是强连通图,但是图2是图1的强连通子图,也就是强连通分量;

    生成树和生成森林

    生成树

    假设一个连通图有n个顶点和e条边,其中n-1条边和n个顶点构成一个极小连通子图,称该极小连通子图为此连通图的生成树;

    图1是一个连通图含有4个顶点和4条边,图2,图3,图4含有3条边和4个顶点,构成了一个极小连通图子图,也称为生成树,为什么是极小连通子图,因为图2,图3,图4中少一条边都构不成一个连通图,多一条边就变成一个回路(环),所以是3条边和4个顶点构成的极小连通子图。图5尽管也是3个边4个顶点,但不是连通图。

    生成森林

    如果一个有向图恰有一个顶点的入度为0,其余顶点的入度为1,则是一颗有向树

    入度为0,相当于根节点,入度为1,相当于分支节点;,比如下面的有向图就是一个有向树

    顶点B的入度是0,其余顶点的入度是1;

    一个有向图的生成森林由若干颗有向树组成,含有图中全部顶点,但有足以构成若干颗不相交的有向树的弧;

    有向图1去掉一些弧后分解成2颗有向树,图2和图3,这两颗树就是有向图图1的生成森林;

    2.图的存储结构

    1).邻接矩阵(数组)表示法

    ① 建立一个顶点表和一个邻接矩阵

    ② 设图 A = (V, E) 有 n 个顶点,则图的邻接矩阵是一个二维数组 A.Edge[n][n]。

    注:在有向图的邻接矩阵中,

       第i行含义:以结点vi为尾的弧(即出度边);

       第i列含义:以结点vi为头的弧(即入度边)。

    邻接矩阵法优点:容易实现图的操作,如:求某顶点的度、判断顶点之间是否有边(弧)、找顶点的邻接点等等。

    邻接矩阵法缺点:n个顶点需要n*n个单元存储边(弧);空间效率为O(n^2)。

    2).邻接表(链式)表示法

    ① 对每个顶点vi 建立一个单链表,把与vi有关联的边的信息(即度或出度边)链接起来,表中每个结点都设为3个域:

    ② 每个单链表还应当附设一个头结点(设为2个域),存vi信息;

    ③ 每个单链表的头结点另外用顺序存储结构存储。

    邻接表的优点:空间效率高;容易寻找顶点的邻接点;

    邻接表的缺点:判断两顶点间是否有边或弧,需搜索两结点对应的单链表,没有邻接矩阵方便。

    3.图的遍历

    遍历定义:从已给的连通图中某一顶点出发,沿着一些边,访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算。

    图的遍历算法求解图的连通性问题、拓扑排序和求关键路径等算法的基础。

    图常用的遍历:一、深度优先搜索;二、广度优先搜索 

    深度优先搜索(遍历)步骤:(如下图)

    ① 访问起始点 v;

    ② 若v的第1个邻接点没访问过,深度遍历此邻接点;

    ③ 若当前邻接点已访问过,再找v的第2个邻接点重新遍历。

    基本思想:——仿树的先序遍历过程。

    遍历结果:v1->v2->v4->v8->v5-v3->v6->v7

    广度优先搜索(遍历)步骤:

    ① 在访问了起始点v之后,依次访问 v的邻接点;

    ② 然后再依次(顺序)访问这些点(下一层)中未被访问过的邻接点;

    ③ 直到所有顶点都被访问过为止。

    遍历结果:v1->v2->v3->v4->v5-v6->v7->v8

    4.图的连通性问题

    1)对无向图进行遍历时,对于连通图,仅需从图中任一顶点出发,进行深度优先搜索或广度优先搜索,便可访问到图中所有顶点。

    2)最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树。

    构造最小生成树有很多算法,但是他们都是利用了最小生成树的同一种性质:MST性质(假设N=(V,{E})是一个连通网,U是顶点集V的一个非空子集,如果(u,v)是一条具有最小权值的边,其中u属于U,v属于V-U,则必定存在一颗包含边(u,v)的最小生成树),下面就介绍两种使用MST性质生成最小生成树的算法:普里姆算法和克鲁斯卡尔算法。

    Kruskal算法特点:将边归并,适于求稀疏网的最小生成树。

    Prime算法特点: 将顶点归并,与边数无关,适于稠密网。

    Prime算法构造最小生成树过程如下图:

    Kruskal算法构造最小生成树过程如下图:

    5.有向无环图及其应用

    有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。

    1)拓扑排序

    拓扑排序对应施工的流程图具有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行。

    我们把顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。

    一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行(对于数据流来说就是死循环)。在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。

    拓扑排序的实现:

    a.在有向图中选一个没有前驱的顶点并且输出

    b.从图中删除该顶点和所有以它为尾的弧(白话就是:删除所有和它有关的边)

    c.重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。

    2)关键路径

    AOE-网是一个带权的有向无环图,其中,顶点表示事件,弧表示活动,权表示活动持续的时间。通常,AOE-网可用来估算工程的完成时间。

    关键路径:在AOE网中,从始点到终点具有最大路径长度(该路径上的各个活动所持续的时间之和)的路径称为关键路径。

    关键活动:关键路径上的活动称为关键活动。关键活动:e[i]=l[i]的活动

    由于AOE网中的某些活动能够同时进行,故完成整个工程所必须花费的时间应该为始点到终点的最大路径长度。关键路径长度是整个工程所需的最短工期。

    与关键活动有关的量

    (1)事件的最早发生时间ve[k]:ve[k]是指从始点开始到顶点vk的最大路径长度。这个长度决定了所有从顶点vk发出的活动能够开工的最早时间。 

    (2)事件的最迟发生时间vl[k]:vl[k]是指在不推迟整个工期的前提下,事件vk允许的最晚发生时间。

    (3)活动的最早开始时间e[i]:若活动ai是由弧<vk vj>表示,则活动ai的最早开始时间应等于事件vk的最早发生时间。因此,有:e[i]=ve[k]

    (4)活动的最晚开始时间l[i]:活动ai的最晚开始时间是指,在不推迟整个工期的前提下, ai必须开始的最晚时间。若ai由弧<vkvj>表示,则ai的最晚开始时间要保证事件vj的最迟发生时间不拖后。因此,有:l[i]=vl[j]-len<vk,vj

    示例如下:

    6.最短路径

    从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径叫做最短路径。

    1)迪杰斯塔拉算法--单源最短路径

     

    所有顶点间的最短路径—用Floyd(弗洛伊德)算法

    第八章:查找

    查找表是称为集合的数据结构。是元素间约束力最差的数据结构:元素间的关系是元素仅共在同一个集合中。(同一类型的数据元素构成的集合)

    1.静态查找表

      1)顺序查找(线性查找)

      技巧:把待查关键字key存入表头或表尾(俗称“哨兵”),这样可以加快执行速度。

    int Search_Seq( SSTable  ST , KeyType  key ){

    ST.elem[0].key =key;

    for( i=ST.length; ST.elem[ i ].key!=key;  - - i  );

          return i;

    } // Search_Seq

    //ASL=(1+n)/2,时间效率为 O(n),这是查找成功的情况:

    顺序查找的特点:

    优点:算法简单,且对顺序结构或链表结构均适用。

           缺点: ASL 太大,时间效率太低。

      2)折半查找(二分查找)——只适用于有序表,且限于顺序存储结构。

      若关键字不在表中,怎样得知并及时停止查找?

      典型标志是:当查找范围的上界≤下界时停止查找。

      ASL的含义是“平均每个数据的查找时间”,而前式是n个数据查找时间的总和,所以:

     

    3)分块查找(索引顺序查找)

    思路:先让数据分块有序,即分成若干子表,要求每个子表中的数据元素值都比后一块中的数值小(但子表内部未必有序)。然后将各子表中的最大关键字构成一个索引表,表中还要包含每个子表的起始地址(即头指针)。

    特点:块间有序,块内无序。

    查找:块间折半,块内线性

    查找步骤分两步进行:

    ① 对索引表使用折半查找法(因为索引表是有序表);

    ② 确定了待查关键字所在的子表后,在子表内采用顺序查找法(因为各子表内部是无序表);

    查找效率ASL分析:

    2.动态查找表

    1)二叉排序树和平衡二叉树

    • 二叉排序树的定义----或是一棵空树;或者是具有如下性质的非空二叉树:

     (1)若它的左子树不空,则左子树上所有结点的值均小于根的值;

     (2)若它的右子树不空,则右子树的所有结点的值均大于根的值;

     (3)它的左右子树也分别为二叉排序树。

    二叉排序树又称二叉查找树。

    二叉排序树的查找过程:

    BiTree SearchBST(BiTree T, KeyType key)

    {

            //在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素,

            //若查找成功,则返回指向该数据元素结点的指针,否则返回空指针

            if ((!T)||EQ(key, T->data.key))  return(T); //查找结束

            else if LT(key, T->data.key) return (SearchBST(T->lchild, key)); //在左子树中继续查找

            else return (SearchBST(T->rchild,key)); //在右子树中继续查找

    }

    • 二叉排序树的插入

            思路:查找不成功,生成一个新结点s,插入到二叉排序树中;查找成功则返回。

       SearchBST (K,  &t) { //K为待查关键字,t为根结点指针

       p=t;       //p为查找过程中进行扫描的指针

       while(p!=NULL)

    {

       case {

                   K= p->data:  {查找成功,return true;}

                   K< p->data :  {q=p;p=p->lchild }  //继续向左搜索

                   K> p->data :  {q=p;p=p->rchild } //继续向右搜索

                }

      }  //查找不成功则插入到二叉排序树中

    s =(BiTree)malloc(sizeof(BiTNode)); 

    s->data=K; s ->lchild=NULL; s ->rchild=NULL;

          //查找不成功,生成一个新结点s,插入到二叉排序树叶子处

    case {

                t=NULL:   t=s;   //若t为空,则插入的结点s作为根结点

                K < q->data: q->lchild=s;  //若K比叶子小,挂左边

                K > q->data: q->rchild=s; //若K比叶子大,挂右边

            }

    return OK;

    }

    • 二叉排序树的删除

    假设:*p表示被删结点的指针; PL和PR 分别表示*P的左、右孩子指针;

    *f表示*p的双亲结点指针;并假定*p是*f的左孩子;则可能有三种情况:

    *p有两颗子树,有两种解决方法:

    法1:令*p的左子树为 *f的左子树,*p的右子树接为*s的右子树;如下图(c)所示  //即 fL=PL  ;   SR=PR   ;

    法2:直接令*p的直接前驱(或直接后继)替代*p,然后再从二叉排序树中删去它的直接前驱(或直接后继) 。如图(d),当以直接前驱*s替代*p时,由于*s只有左子树SL,则在删去*s之后,只要令SL为*s的双亲*q的右子树即可。 // *s为*p左子树最右下方的结点

    删除算法如下:

    Status Delete(BiTree &p)

    {

        //从二叉排序树种删除结点p,并重接它的左或右子树

        if(!p->rchild) //右子树空,只需重接它的左子树

        {

            q=p;

            p=p->lchild;

            free(q);

        }

        else if(!p->lchild) //左子树空,只需重接它的右子树

        {

            q=p;

            p=p->rchild;

            free(q);

        }

        else //左右子树都不空

        {

            q=p; 

            s=p->lchild;

            while(s->rchild)  //转左,然后向右到尽头(找p的直接前驱) 图(b)

            {

                q=s;

                s=s->rchild;

            }

            p->data = s->data; //s指向被删结点的“前驱”

            if(q!=p)  //重接*q的右子树

            {

                q->rchild=s->lchild;

            }

            else  //q=p,说明s->rchild为空(即:p->lchild->rchild为空),重接*q的左子树

            {

                q->lchild=s->lchild;

            }

             delete s;

        }//end else 左右子树都不空

        return TRUE;

    }

    二叉排序树查找分析:和折半查找类似,与给定值比较的关键字个数不超过树的深度。然而,折半查找长度为n的表的判定树是惟一的,而含有n个结点的二叉排序树却不惟一。

    含有n个结点的二叉排序树的平均查找长度和树的形态有关。当先后插入的关键字有序时,构成的二叉排序树蜕变为单支树。树的深度为n,其平均查找长度为(n+1)/2(和顺序查找相同),这是最差的情况。最好的情况是二叉排序树的形态和折半查找的判定树相同,其平均查找长度和log2n成正比。

    (n>=2)

     

    • 平衡二叉树

    又称AVL树,即它或者是一颗空树,或者具有如下性质:它的左子树和右子树都是平衡二叉树,且左子树与右子树的深度之差的绝对值不超过1。

    平衡因子:该结点的左子树的深度减去它的右子树的深度。

    平衡二叉树的特点:任一结点的平衡因子只能取:-1、0 或 1。

    如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。我们称调整平衡过程为平衡旋转

    平衡旋转可以归纳为四类:单向右顺时针旋转(LL);单向左逆时针旋转(RR);双向旋转先左逆时针后右顺时针(LR);双向旋转先右顺时针后左逆时针(RL)

    平衡二叉树查找分析:

    时间复杂度为O(logn)

    3.B-树和B+树

    B+树是应文件系统所需而出的一种B树的变型树。一棵m阶的B+树和m阶的B-树的差异在于:

    1.有n棵子树的结点中含有n个关键字,每个关键字不保存数据,只用来索引,所有数据都保存在叶子节点。

    2.所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

    3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)关键字。

    通常在B+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。

    B-树:一棵m阶的B-树或者是一棵空树,或者是满足下列要求的m叉树:

    • 树中的每个结点至多有m棵子树;

    • 若根结点不是叶子结点,则至少有两棵子树;

    • 除根结点外,所有非终端结点至少有[ m/2 ] ( 向上取整 )棵子树。

    • 所有的非终端结点中包括如下信息的数据(n,A0,K1,A1,K2,A2,….,Kn,An)

    其中:Ki(i=1,2,…,n)为关键码,且Ki < K(i+1),

    Ai 为指向子树根结点的指针(i=0,1,…,n),且指针A(i-1) 所指子树中所有结点的关键码均小于Ki (i=1,2,…,n),An 所指子树中所有结点的关键码均大于Kn。n 为关键码的个数。

    • 所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。

     

    4.哈希表

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

     

    1)哈希函数构造方法

    • 直接定址法

    取关键字或关键字的某个线性函数值为散列地址。

    即 H(key) = key 或 H(key) = a*key + b,其中a和b为常数。

    • 除留余数法

    取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。

    即 H(key) = key % p, p < m。

    • 数字分析法

    当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。

    仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。

    • 平方取中法

    先计算出关键字值的平方,然后取平方值中间几位作为散列地址。

    随机分布的关键字,得到的散列地址也是随机分布的。

    • 折叠法(叠加法)

    将关键字分为位数相同的几部分,然后取这几部分的叠加和(舍去进位)作为散列地址。

    用于关键字位数较多,并且关键字中每一位上数字分布大致均匀。

    • 随机数法

    选择一个随机函数,把关键字的随机函数值作为它的哈希值。

    通常当关键字的长度不等时用这种方法。

    构造哈希函数的方法很多,实际工作中要根据不同的情况选择合适的方法,总的原则是尽可能少的产生冲突

    通常考虑的因素有关键字的长度分布情况哈希值的范围等。

    如:当关键字是整数类型时就可以用除留余数法;如果关键字是小数类型,选择随机数法会比较好。

     

    2)哈希冲突的解决方法

     

    • 开放定址法

    Hi=(H(key) + di) MOD m i=1,2,…,k (k<=m)

    当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探测到开放的地址则表明表中无待查的关键字,即查找失败。

    当冲突发生时,使用某种探查(亦称探测)技术在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到

    按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。

    a.线性探查法

    hi=(h(key)+i) % m ,0 ≤ i ≤ m-1

    基本思想是:探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+1],…,直到 T[m-1],此后又循环到 T[0],T[1],…,直到探查到 有空余地址 或者到 T[d-1]为止。

    b.二次探查法

    hi=(h(key)+i*i) % m,0 ≤ i ≤ m-1

    基本思想是:探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+1^2],T[d+2^2],T[d+3^2],…,等,直到探查到 有空余地址 或者到 T[d-1]为止。缺点是无法探查到整个散列空间。

    c.双重散列法

    hi=(h(key)+i*h1(key)) % m,0 ≤ i ≤ m-1

    基本思想是:探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+h1(d)], T[d + 2*h1(d)],…,等。

    该方法使用了两个散列函数 h(key) 和 h1(key),故也称为双散列函数探查法。

    定义 h1(key) 的方法较多,但无论采用什么方法定义,都必须使 h1(key) 的值和 m 互素,才能使发生冲突的同义词地址均匀地分布在整个表中,否则可能造成同义词地址的循环计算。

    该方法是开放定址法中最好的方法之一。

    • 链接法(拉链法)

    将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为 m,则可将散列表定义为一个由 m 个头指针组成的指针数组 T[0..m-1] 。

    凡是散列地址为 i 的结点,均插入到以 T[i] 为头指针的单链表中。

    T 中各分量的初值均应为空指针。

    在拉链法中,装填因子 α 可以大于 1,但一般均取 α ≤ 1。

    3.哈希表的查找及其分析

    哈希表是实现关联数组(associative array)的一种数据结构,广泛应用于实现数据的快速查找。

    查找过程中,关键字的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。

    影响产生冲突多少有以下三个因素:

    1)哈希函数是否均匀;

    2)处理冲突的方法;

    3)哈希表的加载因子。

     

    第九章:内部排序

    排序:将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列。

    稳定性——若两个记录A和B的关键字值相等,且排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。

    1.插入排序

    思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。

    简言之,边插入边排序,保证子序列中随时都是排好序的。

    1)  直接插入排序

        在已形成的有序表中线性查找,并在适当位置插入,把原来位置上的元素向后顺移。

    时间效率: 因为在最坏情况下,所有元素的比较次数总和为(0+1+…+n-1)→O(n^2)。

    其他情况下也要考虑移动元素的次数。 故时间复杂度为O(n^2)

    空间效率:仅占用1个缓冲单元——O(1)

    算法的稳定性:稳定

    直接插入排序算法的实现:

    void InsertSort ( SqList &L ) 

    { //对顺序表L作直接插入排序

     for ( i = 2;  i <=L.length; i++) //假定第一个记录有序

    {

         L.r[0]= L.r[i];

           j=i-1 ;                      //先将待插入的元素放入“哨兵”位置

         while(L[0] .key<L[j].key)

        {  

            L.r[j+1]= L.r[j];

            j--  ;                    

        }      //只要子表元素比哨兵大就不断后移

        L.r[j+1]= L.r[0];      //直到子表元素小于哨兵,将哨兵值送入

                                     //当前要插入的位置(包括插入到表首)

    }

    }

    2)折半插入排序

    子表有序且为顺序存储结构,则插入时采用折半查找定可加速。

    优点:比较次数大大减少,全部元素比较次数仅为O(nlog2n)。

    时间效率:虽然比较次数大大减少,可惜移动次数并未减少, 所以排序效率仍为O(n^2) 。

    空间效率:仍为 O(1)

    稳 定  性: 稳定

     

    3)希尔排序—不稳定

    基本思想:先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。

    优点:让关键字值小的元素能很快前移,且序列若基本有序时,再用直接插入排序处理,时间效率会高很多。

    时间效率:当n在某个特定范围内,希尔排序所需的比较和移动次数约为n^1.3,当n->无穷,可减少到n(log2n)^2

    空间效率:O(1)

    4)快速排序

    基本思想:从待排序列中任取一个元素 (例如取第一个) 作为中心,所有比它小的元素一律前放,所有比它大的元素一律后放,形成左右两个子表;然后再对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个。此时便为有序序列了。

    优点:因为每趟可以确定不止一个元素的位置,而且呈指数增加,所以特别快!

    前提:顺序存储结构

    时间效率:O(nlog2n) —因为每趟确定的元素呈指数增加

    空间效率:O(log2n)—因为递归要用栈(存每层low,high和pivot)

    稳 定 性: 不 稳 定 — —因为有跳跃式交换。

    算法:

    int partition(SqList &L,int low,int high)

    {

      L.r[0] = L.r[low];

      pivot key = L.r[low].key;

        while(low < high)

        {

         while(low<high&&L.r[high]>=pivot) high--;

         L.r[low] = L.r[high];

         while(low<high&&L.r[low]<=pivot) low++;

         L.r[high] = L.r[low];

        }

        L.r[low] = pivot;

        return low;

    }

     

    5)冒泡排序

    基本思路:每趟不断将记录两两比较,并按“前小后大”(或“前大后小”)规则交换。

    优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一旦下趟没有交换发生,还可以提前结束排序。

    前提:顺序存储结构

    冒泡排序的算法分析:

    时间效率:O(n^2) —因为要考虑最坏情况

    空间效率:O(1) —只在交换时用到一个缓冲单元

    稳 定 性: 稳定  —25和25*在排序前后的次序未改变

    冒泡排序的优点:每一趟整理元素时,不仅可以完全确定一个元素的位置(挤出一个泡到表尾),还可以对前面的元素作一些整理,所以比一般的排序要快。

    选择排序:选择排序的基本思想是:每一趟在后面n-i 个待排记录中选取关键字最小的记录作为有序序列中的第i  个记录。

    6)简单选择排序

    思路异常简单:每经过一趟比较就找出一个最小值,与待排序列最前面的位置互换即可。

    ——首先,在n个记录中选择最小者放到r[1]位置;然后,从剩余的n-1个记录中选择最小者放到r[2]位置;…如此进行下去,直到全部有序为止。

    优点:实现简单

    缺点:每趟只能确定一个元素,表长为n时需要n-1趟

    前提:顺序存储结构

    时间效率:O(n^2)——虽移动次数较少,但比较次数较多

    空间效率:O(1)

    算法稳定性——不稳定

    Void SelectSort(SqList  &L ) 

    {

    for (i=1;  i<L.length; ++i)

    {

         j = SelectMinKey(L,i);  //在L.r[i..L.length]中选择key最小的记录

         if( i!=j )   r[i] <--> r[j]; //与第i个记录交换

          } //for

      }  //SelectSort

     

    7)堆排序

    设有n个元素的序列 k1,k2,…,kn,当且仅当满足下述关系之一时,称之为堆。

    如果让满足以上条件的元素序列 (k1,k2,…,kn)顺次排成一棵完全二叉树,则此树的特点是:树中所有结点的值均大于(或小于)其左右孩子,此树的根结点(即堆顶)必最大(或最小)。

    堆排序算法分析:

    时间效率:O(nlog2n)。因为整个排序过程中需要调用n-1次HeapAdjust( )算法,而算法本身耗时为log2n;

    空间效率:O(1)。仅在第二个for循环中交换记录时用到一个临时变量temp。

    稳定性: 不稳定。

    优点:对小文件效果不明显,但对大文件有效。

     

    8)归并排序----稳定

    将两个或两个以上有序表组合成一个新的有序表。

    时间复杂度:O(nlogn)

    空间复杂度:和待排记录等数量的辅助空间。

     

    9)基数排序

    时间复杂度:对于n各记录(每个记录含d个关键字,每个关键字取值范围为rd个值)进行链式基数排序的时间复杂度为O(d(n+rd)),其中每一趟分配的时间复杂度为O(n),每一趟收集的时间复杂度为O(rd)

    10)各种内部排序方法的比较讨论

    (1) 从平均时间性能看,快速排序最佳,其所需时间最省,但快速排序的最坏情况下的时间性能不如堆排序和快速排序。后两者相比较,在n较大时,归并排序所需时间较堆排序省,但它所需的辅助存储量最多。

    (2)基数排序的时间复杂度可写成O(dn)。因此,它最适用于n值很大而关键字较小的序列。

    (3)从方法的稳定性来比较,基数排序是稳定的内排方法,所需时间复杂度为O(n^2)的简单排序方法也是稳定的,然而,快速排序、堆排序和希尔排序等时间性能较好的排序方法是不稳定的。

    展开全文
  • 严蔚敏数据结构课后参考答案

    千次阅读 多人点赞 2019-11-09 22:46:44
    1.简述下列概念:数据数据元素、数据项、数据对象、数据结构、逻辑结构、存储结构、抽象数据类型。 答案: 数据:是客观事物的符号表示,指所有输入到计算机中并被计算机程序处理的符号的总称。如数学计算中用...
  • 数据结构教程(c语言)(已完结)

    万次阅读 多人点赞 2018-03-27 17:57:25
    数据元素:是数据的基本单位用于完整地描述一个对象 数据对象:是性质相同的数据元素的集合,是数据的一个子集 数据项:是组成数据元素的,有独立含义的,可分割的最小单位 数据结构:是相互之间存在的一种或者...
  • 1.简述下列概念:数据数据元素、数据项、数据对象、数据结构、逻辑结构、存储结构、抽象数据类型。 2.试举一个数据结构的例子,叙述其逻辑结构和存储结构两方面的含义和相互关系。 3.简述逻辑结构的四种基本...
  • 数据结构C语言版(答案)

    万次阅读 多人点赞 2012-09-15 22:27:25
    1.1 简述下列术语:数据数据元素、数据对象、数据结构、存储结构、数据类型和抽象数据类型。 解:数据是对客观事物的符号表示。在计算机科学中是指所有输入到计算机中并被计算机程序处理的符号的总称。  ...
  • 数据流图错误汇总

    千次阅读 2017-09-21 14:00:22
    定义:数据流图(Data Flow Diagram):简称DFD,它从数据传递和加工角度,以图形方式来表达系统的逻辑功能、数据在系统内部的逻辑流向和逻辑变换过程,是结构化系统分析方法的主要表达工具及用于表示软件模型的一种...
  • 数据结构

    千次阅读 2012-06-28 00:24:43
    习题一 一、概念题 (一)单选 1.研究数据结构就是研究( D ) A.数据的逻辑结构 B.数据的存储结构 ...C.数据的逻辑结构和存储结构 ...D.数据的逻辑结构、存储结构及其数据在运算上的实现 ...算法的可行性是指指令不能
  • 网易2018校园招聘数据分析工程师笔试卷(来源:牛客网) 题型 客观题:单选51道,不定项选择12道 完成时间 120分钟 牛客网评估难度系数 3颗星 1. 在软件开发过程中,我们可以采用不同的过程模型,下列有关 增量模型...
  • 开启一个新的系列 —— 「数据分析真题日刷」。七月临近,备战秋招,加油鸭! 今日真题 京东2019春招京东数据分析类试卷(来源:牛客网) 题型 客观题:单选27道,不定项选择3道 完成时间 120分钟 1. 在软件开发...
  • 数据的概括性度量

    千次阅读 2020-03-25 10:41:11
    B、众数用于分类数据 C、一组数据众数是唯一的 D、众数受极端值影响 正确答案: C 3 一组数据排序后处于中间位置上的值称为() A、众数 B、中位数 C、四分位数 D、平均数 正确答案: B 4 一组数据排序后处于25%...
  • 最全的数据结构归纳总结

    千次阅读 多人点赞 2018-12-17 22:05:58
    一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所...数据项:数据可分割的最小单位。一个数据元素可由若干个数据项组成。...
  • SQL Server2005 中的数据类型总结

    千次阅读 2009-08-15 11:15:00
    SQL Server2005 中的数据类型总结 SQL Server 2005 中的数据类型归纳为下列类别:精确数字 bigint decimal int numeric smallint money tinyint smallmon
  • python数据分析(数据可视化)

    万次阅读 多人点赞 2017-11-28 10:12:13
    数据可视化旨在直观展示信息的分析结果和构思,令某些抽象数据具象化,这些抽象数据包括数据测量单位的性质或数量。本章用的程序库matplotlib是建立在Numpy之上的一个Python图库,它提供了一个面向对象的API和一个...
  • 数据分析面试题

    万次阅读 多人点赞 2018-12-14 12:46:24
    数据分析面试题 1.一家超市的顾客数据,将数据可视化并分析销售额和年龄、收入的关系并给出营销建议 年龄 收入 销售额 34 350 123 40 450 114 37 169 135 30 189 139 44 183 117 36 80 121 32 ...
  • 计算机网络数据链路层题库

    万次阅读 多人点赞 2018-07-31 17:21:57
    2 下列属于数据链路层基本功能的有 A、流量控制 B、介质访问控制 C、成帧 D、差错控制 解析: 3 在数据链路层上处理发送方和接收方的收发帧同步问题的是 A、差错控制 B、流量控制 C...
  • 数据结构期末复习必备第一章 绪 论考点习题1.数据结构是一门研究非数值计算程序设计中计算机的 ① 以及它们之间的 ② 和运算等的学科。2.从逻辑上可以把数据结构分为 。3.线性结构的顺序存储结构是一种 ① 的存储...
  • 京东2018秋招数据分析工程师笔试题(来源:牛客网) 题型 客观题:单选18道,不定项选择12道 主观题:编程2道 完成时间 120分钟 牛客网评估难度系数 3颗星 写到「数据分析真题日刷」第七套真题,博客喜迎粉丝啦,...
  • 数据分析岗笔试卷一

    千次阅读 2020-03-21 10:55:30
    京东2019春招数据分析类试卷 考点涉及:软件开发模型、二叉树的遍历、计算机网络TCP/IP、shell、数据库事务的四大特性、索引、机器学习、异常值检测、生成式模型、大数据的三大理念、概率论有关知识等 1、软件开发...
  • 数据结构(C语言)第二版 第一章课后答案 就是这本,我以后也会用,所以趁着考完试做个...1. 简述下列概念:数据数据元素、数据项、数据对象、数据结构、逻辑结构、存储结构、抽象数据类型。 数据是对客观事物的符...
  • Mysql数据结构及算法原理

    万次阅读 2018-04-06 21:01:42
    特别需要说明的是,MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各相同,因此MySQL数据库支持多种索引类型,如BTree索引,哈希索引,全文索引等等。为了避免混乱,本文将只关注于BTree索引,因为这是平常...
  • 根据此书所做随笔笔记。 一、绪论 1.1、数据机构的研究内容 ...由于数据必须在计算机中处理,因此不能局限于数据本身的数学问题的研究,还必须考虑数据的物理结构,即数据在计算机中的存储结构。 1.
  • 数据结构_练习 第1章 绪论

    千次阅读 多人点赞 2018-01-08 23:41:18
    1.简述下列概念:数据数据元素、数据项、数据对象、数据结构、逻辑结构、存储结构、抽象数据类型。 答案: 数据:是客观事物的符号表示,指所有输入到计算机中并被计算机程序处理的符号的总称。如数学计算...
  • 数据分析试题集+答案

    千次阅读 2019-09-14 09:56:08
    一、 选择题(每题2分,合计20分) 1、 请找出数列11,18,38,83…的下一项(C) a.146 b.168 c.171 ...3^2+2,4^2+2,6^2+2,9^2+2,13^2+2 ...3、 下列的抽样方法中,抽样误差最小的是(C) a.单纯随机抽样 b.系统...
  • 视频编解码类型MJPEG数据格式介绍

    千次阅读 2019-12-13 15:32:07
    下列像素密度字段的单位: 00:无单位;width:height像素宽高比=Xdensity:Ydensity 01:每英寸像素(2.54厘米) 02:每厘米像素 Xdensity 2 水平像素...
  • Oracle数据泵详解

    千次阅读 2014-06-25 17:32:18
    数据

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 64,595
精华内容 25,838
关键字:

下列不能用于数据单位的是