Data Structures Algorithms 简明教程

Data Structures & Algorithms - Quick Guide

Overview

Data Structure(数据结构)是一种系统的方式,以便有效利用数据。以下术语是数据结构的基本术语。

  1. Interface − 每个数据结构都有一个接口。接口表示数据结构支持的操作集。一个接口仅提供受支持操作的列表、它们可以接受的参数类型以及这些操作的返回类型。

  2. Implementation - 实现提供了数据结构的内部表示。实现还提供了数据结构操作中使用的算法的定义。

Characteristics of a Data Structure

  1. Correctness − 数据结构实现应该正确地实现其接口。

  2. Time Complexity − 数据结构操作的运行时间或执行时间必须尽可能小。

  3. Space Complexity − 数据结构操作的内存使用量应尽可能小。

Need for Data Structure

随着应用程序变得复杂且数据丰富,如今,应用程序面临着三个常见问题。

  1. Data Search - 考虑一个商店的 100 万(106)件物品的库存。如果应用程序要搜索一件物品,则它必须每次在 100 万(106)件物品中搜索一件物品,这会减慢搜索速度。随着数据的增长,搜索将变得更加缓慢。

  2. Processor speed - 处理器速度尽管非常高,如果数据增长到数十亿条记录,就会受到限制。

  3. Multiple requests - 因为成千上万的用户可以在 Web 服务器上同时搜索数据,即使是速度最快的服务器在搜索数据时也会失败。

为了解决上述问题,数据结构应运而生。数据可以按数据结构组织,按这种方式,可能不需要搜索所有项目,并且可以几乎即时搜索所需数据。

Execution Time Cases

在三个案例中,通常会以相对方式比较各种数据结构的执行时间。

  1. Worst Case − 在此场景中,特定数据结构操作所需的时间最大。如果一个操作最坏情况时的时间为 ƒ(n),则此操作不会超过 ƒ(n) 时间,其中 ƒ(n) 表示 n 的函数。

  2. Average Case − 此场景描述数据结构操作的平均执行时间。如果一个操作执行需要 ƒ(n) 时间,那么 m 个操作将需要 mƒ(n) 时间。

  3. Best Case − 此场景描述数据结构操作尽可能最短的执行时间。如果一个操作执行需要 ƒ(n) 时间,则实际操作可能花费时间为随机数,最大值将为 ƒ(n)。

Basic Terminology

  1. Data − 数据是值或值集。

  2. Data Item − 数据项是指单一值单元。

  3. Group Items − 被拆分成较小单元的数据项称为组项。

  4. Elementary Items − 无法拆分的数据项称为基本项。

  5. Attribute and Entity − 实体是包含特定属性或特性的内容,可以分配值。

  6. Entity Set − 类似属性的实体形成实体集。

  7. Field − 字段是表示实体属性的单个基本信息单元。

  8. Record − 记录是由给定实体的字段值集合。

  9. File − 文件是由给定实体集中的实体记录的集合。

Environment Setup

Local Environment Setup

如果你仍然愿意为 C 编程语言设置环境,则需要在计算机上有以下两个工具:(a)文本编辑器和(b)C 编译器。

Text Editor

这将用于键入您的程序。一些编辑器的示例包括 Windows 记事本、OS 编辑命令、Brief、Epsilon、EMACS 以及 vim 或 vi。

文本编辑器的名称和版本可能因不同的操作系统而异。例如,记事本将用于 Windows,而且 vim 或 vi 也可用于 Windows 以及 Linux 或 UNIX。

您使用编辑器创建的文件称为源文件,其中包含程序源代码。C 程序的源文件通常以扩展名 " .c " 命名。

在开始编程之前,请确保到位文本编辑器,并且你具备编写计算机程序、将其保存在文件中、对其进行编译,最后执行它的经验。

The C Compiler

写在源文件中的源代码是程序的人类可读源代码。它需要“编译”才能变为机器语言,以便 CPU 实际上按照给定的说明执行程序。

此 C 编程语言编译器将用于将你的源代码编译为最终的可执行程序。我们假设你对编程语言编译器具备基础知识。

使用最频繁且免费的编译器是 GNU C/C++ 编译器。否则,如果具有相应的操作系统 (OS),你可以有 Hewlett-Packard (HP) 或 Solaris 的编译器。

以下部分指导如何在各种操作系统上安装 GNU C/C 编译器。我们一同提到 C/C,因为 GNU GCC 编译器同时适用于 C 和 C++ 编程语言。

Installation on UNIX/Linux

如果您使用 Linux or UNIX ,那么通过从命令行输入以下命令来检查您的系统上是否安装了 GCC -

$ gcc -v

如果你在自己的机器上安装了 GNU 编译器,则它应当打印如下消息:

Using built-in specs.
Target: i386-redhat-linux
Configured with: ../configure --prefix = /usr .......
Thread model: posix
gcc version 4.1.2 20080704 (Red Hat 4.1.2-46)

如果未安装 GCC,则您将不得不使用 https://gcc.gnu.org/install/ 中提供的详细说明自己安装它。

本教程是基于 Linux 编写的,并且所有给定的示例都已在 Linux 系统的 Cent OS 版本中编译。

Installation on Mac OS

如果你使用 Mac OS X,获取 GCC 的最简单方法是从 Apple 网站下载 Xcode 开发环境,并遵循简单的安装说明。一旦设置了 Xcode,你将能够使用 GNU 编译器针对 C/C++。

Xcode 当前可从 developer.apple.com/technologies/tools/ 获取。

Installation on Windows

要在 Windows 上安装 GCC,需要安装 MinGW。要安装 MinGW,请访问 MinGW 主页 www.mingw.org ,然后单击 MinGW 下载页面的链接。下载 MinGW 安装程序的最新版本,它应当命名为 MinGW-<版本>.exe。

安装 MinWG 时,您至少必须安装 gcc-core、gcc-g++、binutils 和 MinGW 运行时,但您可能希望安装更多。

将 MinGW 安装的 bin 子目录添加到您的 PATH 环境变量,以便您可以通过其简单名称在命令行中指定这些工具。

安装完成后,你将可以使用 Windows 命令行来运行 gcc、g++、ar、ranlib、dlltool 以及其他一些 GNU 工具。

Data Structure Basics

本章介绍数据结构相关的基本术语。

Data Definition

数据定义通过以下特征定义特定数据。

  1. Atomic - 定义应当定义单个概念。

  2. Traceable - 定义应当能够映射到某个数据元素。

  3. Accurate − 定义应明确。

  4. Clear and Concise − 定义应易于理解。

Data Object

数据对象表示具有数据的一个对象。

Data Type

数据类型是一种分类各种数据类型(例如整数、字符串等)的方式,它确定哪些值可以与相应的数据类型配合使用,哪些类型的操作可以对相应的数据类型执行。有两种数据类型:

  1. Built-in Data Type

  2. Derived Data Type

Built-in Data Type

编程语言内置支持的数据类型称为内置数据类型。例如,大多数语言提供以下内置数据类型。

  1. Integers

  2. Boolean (true, false)

  3. Floating (Decimal numbers)

  4. Character and Strings

Derived Data Type

那些实现无关的数据类型称为派生数据类型,因为它们可以用这样或那样的方式实现。这些数据类型通常是通过组合基本数据类型或内置数据类型和对它们的关联操作而构建的。例如 −

  1. List

  2. Array

  3. Stack

  4. Queue

Basic Operations

数据结构中的数据由某些操作处理。所选的具体数据结构很大程度上取决于需要在数据结构上执行的操作的频率。

  1. Traversing

  2. Searching

  3. Insertion

  4. Deletion

  5. Sorting

  6. Merging

Data Structures and Types

Data structures 在编程语言中引入,目的是存储、组织和操作数据。它们的设计方式让访问和处理数据变得更加容易和简单。这些数据结构不仅限于一种特定的编程语言;它们只是一些在内存中构建数据的代码片段。

数据类型经常被误认为数据结构的一种类型,但即使它们被称为抽象数据类型,也不是完全正确的。数据类型表示数据类型,而数据结构只是一组类似或不同的数据类型。

data structures and types

通常只有两种类型的数据结构 −

  1. Linear

  2. Non-Linear

Linear Data Structures

数据存储在线性数据结构中按顺序。这些是基本结构,因为元素按顺序存储,而无需应用任何数学运算。

linear data structures

线性数据结构通常很容易实现,但由于内存分配可能变得复杂,时间和空间复杂度会增加。一些线性数据结构的示例包括 −

  1. Arrays

  2. Linked Lists

  3. Stacks

  4. Queues

基于数据存储方法,这些线性数据结构分为 staticdynamic 数据结构。

Static Linear Data Structures

在静态线性数据结构中,内存分配不可扩展。一旦使用整个内存,就不能再检索更多空间来存储更多数据。因此,需要根据程序的大小来预留内存。这也将成为一个缺点,因为保留比所需更多的内存会导致内存块浪费。

静态线性数据结构的最佳示例是数组。

Dynamic Linear Data Structures

在动态线性数据结构中,内存分配可以在需要时动态进行。考虑到程序的空间复杂度,这些数据结构是高效的。

一些动态线性数据结构的示例包括:链表、堆栈和队列。

Non-Linear Data Structures

非线性数据结构以层次结构的形式存储数据。因此,与线性数据结构相比,数据可以在多个级别中找到,并且难以遍历。

non linear data structures

但是,它们旨在克服线性数据结构的问题和限制。例如,线性数据结构的主要缺点是内存分配。由于数据在线性数据结构中按顺序分配,因此这些数据结构中的每个元素都使用一个完整的内存块。然而,如果数据使用的内存少于分配的块所能容纳的,那么块中的额外内存空间就会被浪费。因此,非线性数据结构应运而生。它们降低了空间复杂度,并最优地使用了内存。

一些非线性数据结构类型 −

  1. Graphs

  2. Trees

  3. Tries

  4. Maps

Array Data Structure

数组是一种线性数据结构,定义为具有相同或不同数据类型的元素集合。它们以单维和多维形式存在。当需要将多个类似性质的元素一起存储在同一个地方时,这些数据结构才会出现。

arrays1

数组索引和内存地址之间的区别在于,数组索引充当键值来标记数组中的元素。然而,内存地址是可用空闲内存的起始地址。

以下是理解数组概念的重要术语。

  1. Element - 存储在数组中的每个项称为一个元素。

  2. Index - 数组中每个元素的位置都有一个数字索引,用于标识元素。

Syntax

CC++ 编程语言中创建数组 −

data_type array_name[array_size] = {elements separated using commas}
or,
data_type array_name[array_size];

Java 编程语言中创建数组 −

data_type[] array_name = {elements separated by commas}
or,
data_type array_name = new data_type[array_size];

Need for Arrays

数组用作从小型排序问题到更为复杂问题(例如旅行商问题)的许多问题的解决方案。除了数组以外,还有许多能够为这些问题提供有效时间和空间复杂度的数据结构,那么使用数组有什么好处?答案在于随机访问查找时间。

数组提供了 O(1) 的随机访问查找时间。这意味着访问数组的第 1 个索引和数组的第 1000 个索引所需的时间都是一样的。这是由于数组带有指针和偏移值。指针指向内存的正确位置,偏移值表明在所述内存中查找距离。

                           array_name[index]
                              |       |
                           Pointer   Offset

因此,在包含 6 个元素的数组中,要访问第 1 个元素,数组指向第 0 个索引。类似地,要访问第 6 个元素,数组指向第 5 个索引。

Array Representation

数组表示为一系列存储桶,其中每个存储桶存储一个元素。这些存储桶的索引从“0”到“n-1”,其中 n 是该特定数组的大小。例如,大小为 10 的数组将具有索引为 0 到 9 的存储桶。

此索引也将类似于多维数组。如果它是一个二维数组,它将在每个存储桶中具有子存储桶。然后,它将被索引为 array_name[m][n],其中 m 和 n 是数组中每一级的尺寸。

array representation

根据上述说明,以下是要考虑的重要事项。

  1. Index starts with 0.

  2. 数组长度为 9,这意味着它可以存储 9 个元素。

  3. 可以通过其索引访问每个元素。例如,我们可以获取索引 6 处的元素,如 23。

Basic Operations in the Arrays

数组中的基本操作是插入、删除、搜索、显示、遍历和更新。通常执行这些操作以修改数组中的数据或报告数组的状态。

数组支持的基本操作如下。

  1. Traverse - 逐个打印所有数组元素。

  2. Insertion - 在给定的索引处添加一个元素。

  3. Deletion - 删除给定索引处的元素。

  4. Search - 使用给定索引或按值搜索元素。

  5. Update - 更新给定索引处的元素。

  6. Display − 显示数组的内容。

在 C 中,当一个数组用大小初始化时,它会按以下顺序将其默认值分配给其元素。

Insertion Operation

在插入操作中,我们将一个或多个元素添加到数组。根据要求,可以在数组的开头、结尾或任何给定索引处添加新元素。这是使用编程语言的输入语句完成的。

Algorithm

以下是将元素插入线性阵列中的算法,直到我们到达数组的末尾 −

1. Start
2. Create an Array of a desired datatype and size.
3. Initialize a variable ‘i’ as 0.
4. Enter the element at ith index of the array.
5. Increment i by 1.
6. Repeat Steps 4 & 5 until the end of the array.
7. Stop

在这里,我们看到了插入操作的实际实现,我们在其中在数组的末尾添加数据 −

Example

对于数组插入操作的其他变体, click here

Deletion Operation

在此数组操作中,我们将从数组的特定索引处删除一个元素。当我们将后续索引中的值分配给当前索引时,就会发生此删除操作。

Algorithm

考虑 LA 是一个包含 N 个元素的线性数组,而 K 是一个正整数,使得 K⇐N。以下是删除 LA 的第 K 个位置的元素的算法。

1. Start
2. Set J = K
3. Repeat steps 4 and 5 while J < N
4. Set LA[J] = LA[J + 1]
5. Set J = J+1
6. Set N = N-1
7. Stop

Example

以下是该操作在各种编程语言中的实现 −

Search Operation

使用键在数组中搜索元素;键元素顺序比较数组中的每一个值,以检查键是否存在于数组中。

Algorithm

根据 LA 是一个包含 N 个元素的线性数组以及 K 是正整数(使得 K⇐N)的条件。以下为使用顺序搜索查找值 ITEM 的元素的算法。

1. Start
2. Set J = 0
3. Repeat steps 4 and 5 while J < N
4. IF LA[J] is equal ITEM THEN GOTO STEP 6
5. Set J = J +1
6. PRINT J, ITEM
7. Stop

Example

以下是该操作在各种编程语言中的实现 −

Traversal Operation

此操作会遍历数组的所有元素。我们使用循环语句完成此操作。

Algorithm

以下是遍历线性数组中所有元素的算法−

1 Start
2. Initialize an Array of certain size and datatype.
3. Initialize another variable ‘i’ with 0.
4. Print the ith value in the array and increment i.
5. Repeat Step 4 until the end of the array is reached.
6. End

Example

以下是该操作在各种编程语言中的实现 −

Update Operation

更新操作是指在给定索引处从数组中更新现有元素。

Algorithm

根据 LA 是一个包含 N 个元素的线性数组以及 K 是正整数(使得 K⇐N)的条件。以下为更新 LA 的第 K 个位置的元素的算法。

1. Start
2. Set LA[K-1] = ITEM
3. Stop

Example

以下是该操作在各种编程语言中的实现 −

Display Operation

此操作会使用 print 语句显示整个数组中的所有元素。

Algorithm

1. Start
2. Print all the elements in the Array
3. Stop

Example

以下是该操作在各种编程语言中的实现 −

Linked List Data Structure

如果数组容纳的数据类型相似,则链表包含数据类型不同的元素,且按顺序排列。

但如何创建这些链表?

链表是通过链接连接在一起的“节点”集合。这些节点包含要存储的数据以及链表中下一个节点地址的指针。在数组中,大小限制在定义中,但在链表中没有定义的大小。任何数量的数据都可以存储在链表中并可从中删除。

有三种类型的链表−

  1. Singly Linked List − 节点仅指向列表中下一个节点的地址。

  2. Doubly Linked List − 节点指向先前和下一个节点的地址。

  3. Circular Linked List − 列表中的最后一个节点将指向列表中的第一个节点。它可以是单向链接或双向链接。

Linked List Representation

可以将链表可视化为一个节点链,其中每个节点指向下一个节点。

linked list representation

根据上述说明,以下是要考虑的重要事项。

  1. 链表包含称为 first(头)的链接元素。

  2. 每个链接包含数据字段和称为 next 的链接字段。

  3. 每条链接使用其 next 链接与其下一个链接相连。

  4. 最后一个链接包含一个链接为 null,以标记列表的结尾。

Types of Linked List

以下为不同类型的链表。

Singly Linked Lists

单链表在每个节点中包含两个“存储单元”;一个存储单元保存数据,而另一个存储单元保存列表中下一个节点的地址。只能向一个方向遍历,因为同一列表的两个节点之间只存在一个链接。

singly linked lists

Doubly Linked Lists

双链表在每个节点中包含三个“存储单元”;一个存储单元保存数据,而其他存储单元保存列表中前一个和下一个节点的地址。列表被遍历两次,因为列表中的节点从两侧相互连接。

doubly linked lists

Circular Linked Lists

循环链表可存在于单向链表和双向链表。

由于循环链表中的最后一个节点与第一个节点相连接,因此对该链表中的遍历将一直继续,直到它被中断。

circular linked lists

Basic Operations in the Linked Lists

链表中的基本操作包括插入、删除、搜索、显示以及删除指定关键处的元素。这些操作在单向链表中执行,如下所示:

  1. Insertion - 在列表开头添加一个元素。

  2. Deletion - 删除列表开头的一个元素。

  3. Display - 显示完整列表。

  4. Search - 使用指定的关键搜索元素。

  5. Delete - 使用指定的关键删除元素。

Insertion Operation

在链表中添加一个新节点是多步活动。我们将在此处借助图表了解此内容。首先,使用相同结构创建一个节点,并找到要插入该节点的位置。

insertion operation

想象一下,我们在 A(LeftNode)和 C(RightNode)之间插入一个节点 B(NewNode)。然后将 B.next 指向 C -

NewNode.next −> RightNode;

它应该看起来像这样 -

inserting a node

现在,左侧的下一个节点应该指向新节点。

LeftNode.next −> NewNode;
point to the new node

这会将新节点放在两个节点中间。新列表应该看起来像这样 -

可以在链表中以三种不同的方式进行插入。解释如下:

Insertion at Beginning

在此操作中,我们在列表开头添加了一个元素。

Algorithm

1. START
2. Create a node to store the data
3. Check if the list is empty
4. If the list is empty, add the data to the node and assign the head pointer to it.
5 If the list is not empty, add the data to a node and link to the current head. Assign the head to the newly added node.
6. END

Example

以下是该操作在各种编程语言中的实现 −

Insertion at Ending

在此操作中,我们在列表结尾添加了一个元素。

Algorithm

1. START
2. Create a new node and assign the data
3. Find the last node
4. Point the last node to new node
5. END

Example

以下是该操作在各种编程语言中的实现 −

Insertion at a Given Position

在此操作中,我们在列表中的任意位置添加了一个元素。

Algorithm

1. START
2. Create a new node and assign data to it
3. Iterate until the node at position is found
4. Point first to new first node
5. END

Example

以下是该操作在各种编程语言中的实现 −

Deletion Operation

删除也是一个多步过程。我们将以图片表示形式进行学习。首先,使用搜索算法找到要删除的目标节点。

deletion operation

现在,目标节点的左侧(前一个)节点应该指向目标节点的下一个节点 -

LeftNode.next −> TargetNode.next;
linked list deletion

这将删除指向目标节点的链接。现在,使用以下代码,我们将删除目标节点指向的内容。

TargetNode.next −> NULL;
pointing target node

我们需要使用已删除的节点。我们可以将它保留在内存中,否则我们可以简单地释放内存并完全擦除目标节点。

use deleted node
data items

如果节点被插入列表的开头,则应采取类似的步骤。在将其插入末尾时,列表的倒数第二个节点应指向新节点,并且新节点将指向 NULL。

链表中的删除也通过三种不同的方式执行。它们如下 -

Deletion at Beginning

在此链表的删除操作中,我们正在从列表的开头删除元素。为此,我们将头部指向第二个节点。

Algorithm

1. START
2. Assign the head pointer to the next node in the list
3. END

Example

以下是该操作在各种编程语言中的实现 −

Deletion at Ending

在此链表的删除操作中,我们正在从列表的结尾删除元素。

Algorithm

1. START
2. Iterate until you find the second last element in the list.
3. Assign NULL to the second last element in the list.
4. END

Example

以下是该操作在各种编程语言中的实现 −

Deletion at a Given Position

在此链表的删除操作中,我们正在从列表的任何位置删除元素。

Algorithm

1. START
2. Iterate until find the current node at position in the list
3. Assign the adjacent node of current node in the list to its previous node.
4. END

Example

以下是该操作在各种编程语言中的实现 −

Reverse Operation

此操作是全面的。我们需要使最后一个节点由头节点指向并反转整个链表。

reverse operation

首先,我们遍历到列表的末尾。它应指向 NULL。现在,我们将其指向其前一个节点 -

traverse to the end

我们必须确保最后一个节点不是最后一个节点。因此,我们将拥有一个 temp 节点,它看起来像指向最后一个节点的头节点。现在,我们将使所有左侧节点逐一指向其前一个节点。

temp node

除了由头节点指向的节点(第一个节点),所有节点都应指向其前驱,使其成为其新的后继。第一个节点将指向 NULL。

point to null

我们将使头节点使用 temp 节点指向新的第一个节点。

the temp node

Algorithm

反转链表的分步过程如下 -

1 START
2. We use three pointers to perform the reversing: prev, next, head.
3. Point the current node to head and assign its next value to the prev node.
4. Iteratively repeat the step 3 for all the nodes in the list.
5. Assign head to the prev node.

Example

以下是该操作在各种编程语言中的实现 −

Search Operation

使用键元素在列表中搜索元素。此操作与数组搜索相同;比较列表中的每个元素与给定的键元素。

Algorithm

1 START
2 If the list is not empty, iteratively check if the list contains the key
3 If the key element is not present in the list, unsuccessful search
4 END

Example

以下是该操作在各种编程语言中的实现 −

Traversal Operation

遍历操作按顺序遍历列表中的所有元素,并按该顺序显示元素。

Algorithm

1. START
2. While the list is not empty and did not reach the end of the list, print the data in each node
3. END

Example

以下是该操作在各种编程语言中的实现 −

Complete implementation

Doubly Linked List Data Structure

双向链表是链表的一个变体,与单向链表相比,双向链表可以轻松地双向导航。以下是理解双向链表概念的一些重要术语。

  1. Link - 链表的每个链接都可以存储一个称为元素的数据。

  2. Next - 链表的每个链接都包含一个链接到下一个链接,称为 Next。

  3. Prev - 链表的每个链接都包含一个链接到前一个链接,称为 Prev。

  4. Linked List − 链表包含连接到称为 First 的第一个链接和称为 Last 的最后一个链接的连接链接。

Doubly Linked List Representation

doubly linked list representation

根据上述说明,以下是要考虑的重要事项。

  1. 双链表包含称为 first 和 last 的链接元素。

  2. 每个链接包含数据字段和称为 next 的链接字段。

  3. 每条链接使用其 next 链接与其下一个链接相连。

  4. 每条链接使用其 prev 链接与其前一个链接相连。

  5. 最后一条链接带有 null 作为链接,以标记列表的末尾。

Basic Operations

以下是列表支持的基本操作。

  1. Insertion - 在列表开头添加一个元素。

  2. Deletion - 删除列表开头的一个元素。

  3. Insert Last − 在列表末尾添加一个元素。

  4. Delete Last − 从列表末尾删除一个元素。

  5. Insert After − 在列表项后添加一个元素。

  6. Delete − 使用键从列表中删除元素。

  7. Display forward − 以向前的方式显示完整列表。

  8. Display backward − 以向后方式显示完整列表。

Insertion at the Beginning

在此操作中,我们创建了一个包含三个区室的新节点,其中一个包含数据,另一个包含列表中其前一个和下一个节点的地址。这个新节点插入列表开头。

Algorithm

1. START
2. Create a new node with three variables: prev, data, next.
3. Store the new data in the data variable
4. If the list is empty, make the new node as head.
5. Otherwise, link the address of the existing first node to the next variable of the new node, and assign null to the prev variable.
6. Point the head to the new node.
7. END

Example

以下是该操作在各种编程语言中的实现 −

Deletion at the Beginning

此删除操作会删除双向链表中现有的第一个节点。头指针移到下一个节点,并移除链接。

Algorithm

1. START
2. Check the status of the doubly linked list
3. If the list is empty, deletion is not possible
4. If the list is not empty, the head pointer is shifted to the next node.
5. END

Example

以下是该操作在各种编程语言中的实现 −

Insertion at the End

在此插入操作中,新输入节点将添加到双向链表的末尾;如果列表不为空。如果列表为空,则头指针将指向新节点。

Algorithm

1. START
2. If the list is empty, add the node to the list and point the head to it.
3. If the list is not empty, find the last node of the list.
4. Create a link between the last node in the list and the new node.
5. The new node will point to NULL as it is the new last node.
6. END

Example

以下是该操作在各种编程语言中的实现 −

Complete implementation

Circular Linked List Data Structure

Circular Linked List 是链表的一个变体,其中第一个元素指向最后一个元素,而最后一个元素指向第一个元素。单向链表和双向链表都可以变成循环链表。

Singly Linked List as Circular

在单向链表中,最后一个节点的下一个指针指向第一个节点。

singly linked list as circular

Doubly Linked List as Circular

在双向链表中,最后一个节点的下一个指针指向第一个节点,而第一个节点的前一个指针指向最后一个节点,使两者都成为循环。

doubly linked list as circular

根据上述说明,以下是要考虑的重要事项。

  1. 单链表和双向链表的最后链接的下一个指针在两种情况下都指向列表的第一个链接。

  2. 在双向链表的情况下,第一个链接的前一个指针指向列表的最后一个指针。

Basic Operations

循环列表支持以下重要操作。

  1. insert − 在列表开头插入一个元素。

  2. delete − 从列表开头删除一个元素。

  3. display − 显示列表。

Insertion Operation

循环链表的插入操作只在列表开头插入元素。这不同于通常的单链表和双向链表,因为此列表中没有特定的开始和结束点。插入在列表的开头或特定节点(或给定位置)之后。

Algorithm

1. START
2. Check if the list is empty
3. If the list is empty, add the node and point the head to this node
4. If the list is not empty, link the existing head as the next node to the new node.
5. Make the new node as the new head.
6. END

Example

以下是该操作在各种编程语言中的实现 −

Deletion Operation

循环链表的删除操作可以从列表中删除特定节点。这种类型的列表中的删除操作可以在开始处或给定位置或结束处执行。

Algorithm

1. START
2. If the list is empty, then the program is returned.
3. If the list is not empty, we traverse the list using a current pointer that is set to the head pointer and create another pointer previous that points to the last node.
4. Suppose the list has only one node, the node is deleted by setting the head pointer to NULL.
5. If the list has more than one node and the first node is to be deleted, the head is set to the next node and the previous is linked to the new head.
6. If the node to be deleted is the last node, link the preceding node of the last node to head node.
7. If the node is neither first nor last, remove the node by linking its preceding node to its succeeding node.
8. END

Example

以下是该操作在各种编程语言中的实现 −

Display List Operation

显示列表操作访问列表中的每个节点,并将它们全部打印在输出中。

Algorithm

1. START
2. Walk through all the nodes of the list and print them
3. END

Example

以下是该操作在各种编程语言中的实现 −

Complete implementation

Stack Data Structure

是一种抽象数据类型 (ADT),在大多数编程语言中都很流行。它之所以被称为栈,是因为它具有与现实世界栈类似的操作,例如一叠卡片或一堆盘子等。

stack example

栈遵循后进先出的 (LIFO) 结构,其中插入的最后一个元素将成为第一个被删除的元素。

Stack Representation

栈 ADT 只允许在一端进行所有数据操作。在任何给定时间,我们只能访问栈的顶部元素。

下图描绘了一个栈及其操作:

stack representation

可以通过数组、结构、指针和链表来实现栈。栈可以是固定大小的,也可以具有动态调整大小的能力。这里,我们要使用数组来实现栈,这使其成为固定大小的栈实现。

Basic Operations on Stacks

栈操作通常用于执行栈 ADT 的初始化、使用和去初始化。

栈 ADT 中最基本的运算包括:push()、pop()、peek()、isFull()、isEmpty()。这些都是用于执行数据操作和检查栈状态的内置运算。

栈使用始终指向栈内最顶层元素的指针,因此称为 top 指针。

Insertion: push()

push() 是一种将元素插入栈中的运算。以下是使用更简单的方式描述 push() 运算的算法。

Algorithm

1 − Checks if the stack is full.
2 − If the stack is full, produces an error and exit.
3 − If the stack is not full, increments top to point next empty space.
4 − Adds data element to the stack location, where top is pointing.
5 − Returns success.

Example

以下是该操作在各种编程语言中的实现 −

Note − 在 Java 中,我们已经使用内置方法 push() 来执行此操作。

Deletion: pop()

pop() 是一种从栈中删除元素的数据操作运算。以下伪代码使用更简单的方式描述了 pop() 运算。

Algorithm

1 − Checks if the stack is empty.
2 − If the stack is empty, produces an error and exit.
3 − If the stack is not empty, accesses the data element at which top is pointing.
4 − Decreases the value of top by 1.
5 − Returns success.

Example

以下是该操作在各种编程语言中的实现 −

Note − 在 Java 中,我们使用内置方法 pop()。

peek()

peek() 是一种在不删除最顶层元素的情况下检索栈内最顶层元素的运算。此操作用于借助顶指针检查栈的状态。

Algorithm

1. START
2. return the element at the top of the stack
3. END

Example

以下是该操作在各种编程语言中的实现 −

isFull()

isFull() 运算检查栈是否已满。此操作用于借助顶指针检查栈的状态。

Algorithm

1. START
2. If the size of the stack is equal to the top position of the stack, the stack is full. Return 1.
3. Otherwise, return 0.
4. END

Example

以下是该操作在各种编程语言中的实现 −

isEmpty()

isEmpty() 操作验证堆栈是否为空。此操作用于在顶部指针的帮助下检查堆栈的状态。

Algorithm

1. START
2. If the top value is -1, the stack is empty. Return 1.
3. Otherwise, return 0.
4. END

Example

以下是该操作在各种编程语言中的实现 −

Complete implementation

Expression Parsing

编写算术表达式的方​​法称为 notation 。算术表达式可以用三个不同但等效的符号编写,即不改变表达式的本质或输出。这些符号是 −

  1. Infix Notation

  2. Prefix (Polish) Notation

  3. Postfix (Reverse-Polish) Notation

这些符号以它们在表达式中如何使用运算符命名。我们将在本章中学习同样的内容。

Infix Notation

我们在 infix 符号中编写表达式,例如 a - b + c,其中运算符用于在操作数之间 in 。对于我们人类来说,阅读、编写和用中缀表示法说话很容易,但对于计算设备来说却并非如此。处理中缀表示法的算法在时间和空间消耗方面可能既困难又昂贵。

Prefix Notation

在此表示法中,运算符是 prefix*ed to operands, i.e. operator is written ahead of operands. For example, *+ab 。这等同于它的中缀表示法 a + b 。前缀表示法也称为 Polish Notation

Postfix Notation

这种符号样式称为 Reversed Polish Notation 。在这种符号样式中,运算符是 postfix*ed to the operands i.e., the operator is written after the operands. For example, *ab+ 。这等同于它的中缀表示法 a + b

下表简要尝试显示所有三种符号之间的差异 −

Parsing Expressions

正如我们所讨论的,设计一个算法或程序来解析中缀符号并不是一种非常有效的方法。相反,这些中缀表示法首先转换为后缀或前缀表示法,然后计算。

为了解析任何算术表达式,我们还需要注意运算符的优先级和结合性。

Precedence

当一个操作数位于两个不同的运算符之间时,首先取得操作数的运算符是由一个运算符对另一个运算符的优先级决定的。例如 −

operator precedence

由于乘法运算优先级高于加法,因此 b * c 将首先得到评估。运算符优先级表将在后面提供。

Associativity

关联性描述了具有相同优先级的运算符在表达式中出现时的规则。例如,在表达式 a + b- c 中,+ 和 - 都具有相同的优先级,那么表达式的哪一部分将首先被求值,将由这些运算符的关联性决定。这里,+ 和 - 都是左关联的,因此表达式将求值为 (a + b) − c

优先级和结合性决定了表达式求值顺序。以下是运算符优先级和结合性表(从最高到最低)-

上表显示了运算符的默认行为。在表达式求值的任何时间点,都可以通过使用括号来更改顺序。例如 −

a + b*c 中,表达式部分 b * c 将首先得到评估,其中乘法的优先级高于加法。我们在这里使用括号来使 a + b 首先得到评估,例如 (a + b)*c

Postfix Evaluation Algorithm

现在,我们将研究如何评估后缀表示法的算法 −

Step 1 − scan the expression from left to right
Step 2 − if it is an operand push it to stack
Step 3 − if it is an operator pull operand from stack and perform operation
Step 4 − store the output of step 3, back to stack
Step 5 − scan the expression until all operands are consumed
Step 6 − pop the stack and perform operation

Complete implementation

Queue Data Structure

队列与栈类似,也是一种抽象数据结构。使队列与栈不同的一件事是队列两端都是开放的。因此,它遵循先进先出 (FIFO) 结构,即首先插入的数据项也将首先被访问。数据通过一端插入队列,并从另一端删除 。

car

队列的实际示例可以是单车道单行道,其中的车辆先进入,先退出。更多实际示例可以看作是售票窗口和公共汽车站的队列。

Representation of Queues

类似于堆栈 ADT,队列 ADT 也可以使用数组、链表或指针实现。作为本教程中的一个小示例,我们使用一维数组实现队列。

Representation of queues

Basic Operations

队列操作还包括队列的初始化、使用和永久从内存中删除数据。

队列 ADT 中最基本的操作包括:enqueue()、dequeue()、peek()、isFull()、isEmpty()。这些都是内置操作,用于执行数据操作和检查队列的状态。

队列使用两个指针: frontrear 。前指针从前端访问数据(帮助入队),而后指针从后端访问数据(帮助出队)。

Insertion operation: enqueue()

enqueue() 是一种数据操作操作,用于将元素插入堆栈。以下算法以更简单的方式描述了 enqueue() 操作。

Algorithm

1 − START
2 – Check if the queue is full.
3 − If the queue is full, produce overflow error and exit.
4 − If the queue is not full, increment rear pointer to point the next empty space.
5 − Add data element to the queue location, where the rear is pointing.
6 − return success.
7 – END

Example

以下是该操作在各种编程语言中的实现 −

Deletion Operation: dequeue()

dequeue() 是一种数据处理操作,用于从堆栈中移除元素。以下算法以一种更简单的方式描述了 dequeue() 操作。

Algorithm

1 – START
2 − Check if the queue is empty.
3 − If the queue is empty, produce underflow error and exit.
4 − If the queue is not empty, access the data where front is pointing.
5 − Increment front pointer to point to the next available data element.
6 − Return success.
7 – END

Example

以下是该操作在各种编程语言中的实现 −

The peek() Operation

peek() 是一种操作,用于检索队列中最前面的元素,而不会删除它。此操作用于在指针的帮助下检查队列的状态。

Algorithm

1 – START
2 – Return the element at the front of the queue
3 – END

Example

以下是该操作在各种编程语言中的实现 −

The isFull() Operation

isFull() 操作验证堆栈是否已满。

Algorithm

1 – START
2 – If the count of queue elements equals the queue size, return true
3 – Otherwise, return false
4 – END

Example

以下是该操作在各种编程语言中的实现 −

The isEmpty() operation

isEmpty() 操作验证堆栈是否为空。此操作用于在顶部指针的帮助下检查堆栈的状态。

Algorithm

1 – START
2 – If the count of queue elements equals zero, return true
3 – Otherwise, return false
4 – END

Example

以下是该操作在各种编程语言中的实现 −

Implementation of Queue

在本章中,队列数据结构的算法实现是在四种编程语言中执行的。

Graph Data Structure

图是一种抽象数据类型 (ADT),它由一组通过链接相互连接的对象组成。这些对象称为 vertices ,而链接称为 edges

通常,一个图表示为 G = {V,E},其中 G 是图空间,V 是顶点集合,E 是边集合。如果 E 为空,则该图被称为 forest

在我们继续之前,让我们熟悉一些重要的术语 −

  1. Vertex − 图的每个节点都表示为一个顶点。在以下示例中,标记的圆圈表示顶点。因此,A 到 G 是顶点。我们可以使用一个数组来表示它们,如下面的图像所示。这里 A 可以通过索引 0 标识。B 可以使用索引 1 进行标识,依此类推。

  2. Edge − 边表示两个顶点之间的路径或两条顶点之间的线。在以下示例中,从 A 到 B,从 B 到 C 的线等表示边。我们可以使用一个二维数组来表示一个数组,如下面的图像所示。这里 AB 可以在第 0 行,第 1 列中表示为 1,BC 在第 1 行,第 2 列中表示为 1,依此类推,而其他组合保持为 0。

  3. Adjacency − 如果两个节点或顶点通过一条边相互连接,则它们是相邻的。在以下示例中,B 邻接到 A,C 邻接到 B,依此类推。

  4. Path − 路径表示两个顶点之间的边缘序列。在以下示例中,ABCD 表示从 A 到 D 的路径。

graph

Operations of Graphs

图的主要操作包括使用顶点和边创建图,并显示所述图。但是,使用图执行的最常见和最流行的操作之一是遍历,即按特定顺序访问图的每个顶点。

图中有两种类型的遍历 −

  1. Depth First Search Traversal

  2. Breadth First Search Traversal

Depth First Search Traversal

深度优先搜索是一种遍历算法,它以递减的深度顺序访问图的所有顶点。在此算法中,选择一个任意节点作为起点,并通过标记未访问的相邻节点来遍历该图,直到标记所有顶点为止。

DFS 遍历使用堆栈数据结构来跟踪未访问的节点。

Breadth First Search Traversal

广度优先搜索是一种遍历算法,它在移动到下一个深度级别之前,先访问深度一个级别的图的所有顶点。在此算法中,选择一个任意节点作为起点,并通过访问同一深度级别的相邻顶点并标记它们来遍历图,直到没有剩余顶点为止。

DFS 遍历使用队列数据结构来跟踪未访问的节点。

Representation of Graphs

在表示图表时,我们必须仔细描绘图表中存在的元素(顶点和边)以及它们之间的关系。在图片上,用一组有限的节点和它们之间的连接链接来表示一个图表。但是,我们也可以用其他最常用的方式来表示图表,例如 −

  1. Adjacency Matrix

  2. Adjacency List

Adjacency Matrix

邻接矩阵是一个 V×V 矩阵,其中值填充为 0 或 1。如果 Vi 和 Vj 之间存在链接,则记录为 1;否则记录为 0。

对于下面给出的图表,让我们构建一个邻接矩阵 −

Adjacency Matrix

邻接矩阵为 −

adjacency matrix representation

Adjacency List

邻接列表是直接连接到图中其他顶点的顶点列表。

Adjacency Matrix

邻接表是 −

adjacency list

Types of graph

有两种基本类型的图——

  1. Directed Graph

  2. Undirected Graph

有向图,顾名思义,由具有从顶点指向外部或指向顶点的方向的边线组成。无向图有完全无方向的边线。

Directed Graph

Directed Graph

undirected graph

Undirected Graph

Spanning Tree

spanning tree 是无向图的一个子集,它包含图中所有用图中最少数量的边线连接的顶点。精确地说,生成树的边线是原始图中的边线的一个子集。

如果图中的所有顶点是连接的,那么至少存在一棵生成树。在一个图中,可能存在多棵生成树。

Properties

  1. 生成树没有任何循环。

  2. 可以从任何其他顶点到达任何顶点。

Example

在下列图中,高亮的边线形成了生成树。

spanning tree

Minimum Spanning Tree

Minimum Spanning Tree (MST) 是连通的加权无向图的边线的子集,它用最小的总边线权重将所有顶点连接在一起。为了推导 MST,可以使用 Prim 算法或 Kruskal 算法。因此,我们将在本章讨论 Prim 算法。

正如我们所讨论的,一个图可能有多棵生成树。如果有 n 个顶点,生成树应有 n−1 个边线。在此上下文中,如果图的每个边线都与一个权重相关联,并且存在多棵生成树,我们需要找到图的最小生成树。

此外,如果存在任何重复的加权边线,图可能有多个最小生成树。

minimum spanning tree

在上述图中,我们展示了一棵生成树,尽管它不是最小生成树。这棵生成树的成本是 (5+7+3+3+5+8+3+4)=38

Shortest Path

图中最短的路径被定义为从一个顶点到另一个顶点的最小成本路线。这在加权有向图中最常见,但也可以应用于无向图。

查找图中最短路径的一个流行实际应用是地图。在各种最短路径算法的帮助下,导航变得更加容易和简单,这些算法将目的地视为图的顶点,将路线视为边线。两种常见的最短路径算法是——

  1. Dijkstra’s Shortest Path Algorithm

  2. Bellman Ford 最短路径算法

Example

以下是该操作在各种编程语言中的实现 −

Depth First Traversal

深度优先搜索 (DFS) 算法以深度方向遍历图,并使用栈来记住以在任何迭代中发生死锁时获取要开始搜索的下一个顶点。

depth first traversal

正如上面给出的示例中,DFS 算法首先从 S 到 A 到 D 到 G 到 E 到 B,然后到 F,最后到 C。它采用以下规则。

  1. Rule 1 - 访问邻接的未访问顶点。将其标记为已访问。显示它。将其推入堆栈。

  2. Rule 2 - 如果未找到邻接顶点,则从堆栈弹出一个顶点。(它将弹出堆栈中所有没有邻接顶点的顶点。)

  3. Rule 3 - 重复规则 1 和规则 2,直到堆栈为空。

由于 C 没有任何未访问的邻接节点,因此我们将继续弹出堆栈,直到找到具有未访问邻接节点的节点。在这种情况下,没有,并且我们一直弹出,直到堆栈为空。

Example

Breadth First Traversal

广度优先搜索 (BFS) 算法以广度方向遍历图,并使用队列来记住以在任何迭代中发生死锁时获取要开始搜索的下一个顶点。

breadth first traversal

正如以上给出的示例,BFS 算法首先从 A 到 B 到 E 到 F,然后到 C 和 G,最后到 D。它采用以下规则。

  1. Rule 1 − 访问相邻的未访问过的顶点。将其标识为已访问。显示它。将其插入队列。

  2. Rule 2 − 如果未找到相邻顶点,则从队列中删除第一个顶点。

  3. Rule 3 − 重复规则 1 和规则 2,直至队列为空。

在此阶段,我们只剩下未标记(未访问)的节点。但根据算法,我们会不断出列以获取所有未访问的节点。当队列清空后,程序结束。

Example

Spanning Tree

生成树是图 G 的一个子集,它以尽可能少数量的边覆盖所有顶点。因此,生成树没有回路并且不能断开连接。

根据这个定义,我们可以得出结论:每个相连且无向的图 G 至少有一个生成树。断开连接的图没有任何生成树,因为它无法跨越到其所有顶点。

spanning trees

我们从一个完全图中找到了三个生成树。一个完整的无向图可以有 nn-2 个生成树,其中 n 是节点数。在上述示例中, n is 3, 因此 33−2 = 3 生成树是可能的。

General Properties of Spanning Tree

我们现在明白一个图可以有多个生成树。以下是与图 G 相连的生成树的一些性质 −

  1. 连通图 G 可以有多个生成树。

  2. 图 G 所有的可能的生成树具有相同的边和顶点数。

  3. 生成树没有任何回路(环)。

  4. 从生成树中移除一条边会使图断开连接,即生成树为 minimally connected

  5. 向生成树中添加一条边会创建一个回路或环,即生成树为 maximally acyclic

Mathematical Properties of Spanning Tree

  1. 生成树有 n-1 条边,其中 n 是节点(顶点)数。

  2. 通过从一个完全图中移除 e - n + 1 条边,我们能构建一个生成树。

  3. 一个完全图可以有最多 nn-2 个生成树。

因此,我们可以得出结论:生成树是连通图 G 的一个子集,断开连接的图没有生成树。

Application of Spanning Tree

生成树本质上用于查找一条最短路径以连接图中的所有节点。生成树的常见应用包括 −

  1. Civil Network Planning

  2. Computer Network Routing Protocol

  3. Cluster Analysis

让我们通过一个小例子来理解一下。考虑城市网络是一个巨大的图,现在计划以一种方式布设电话线路,即,我们可以用最少的线路连接到所有城市节点。这就是生成树派上用场的地方。

Minimum Spanning Tree (MST)

在一个带权重的图中,极小生成树是生成树,其权重比相同图形的所有其他生成树的权重都小。在现实世界中,此权重可以衡量为距离、拥堵、交通负荷或赋予边的任何任意值。

Minimum Spanning-Tree Algorithm

我们将在此处了解最重要的两种生成树算法:

两种算法均为贪心算法。

Tree Data Structure

树是一个具有基于层次结构的非线性抽象数据类型。它包括通过链接连接的存储数据的节点。树数据结构源自一个称为根节点的单个节点,并具有连接到根节点的子树。

tree data structure

Important Terms

以下是关于树的一些重要术语。

  1. Path - 路径是指沿着树的边的一系列节点。

  2. Root - 树顶部的节点称为根。每棵树只有一个根,且从根节点到任何节点只有一条路径。

  3. Parent - 除了根节点之外的任何节点向上只有一个边到一个称为父节点的节点。

  4. Child - 通过其向下边缘连接在给定节点之下的节点称为其子节点。

  5. Leaf - 没有子节点的节点称为叶节点。

  6. Subtree - 子树表示节点的后代。

  7. Visiting - 访问是指在控件位于节点上时检查节点的值。

  8. Traversing − 遍历指按照特定顺序经过节点。

  9. Levels - 节点的级别表示节点的代。如果根节点位于第 0 级,则其下一个子节点位于第 1 级,而其孙节点位于第 2 级,依此类推。

  10. Keys - 键表示节点的值,基于该值对节点执行搜索操作。

Types of Trees

有三种类型的树:

  1. General Trees

  2. Binary Trees

  3. Binary Search Trees

General Trees

通用树是无序树数据结构,其中根节点具有最小 0 或最大“n”个子树。

通用树对其层次结构没有限制。因此,根节点充当所有其他子树的超集。

General Trees

Binary Trees

二叉树是在其中根节点最多只能容纳 2 个子树(左子树和右子树)的通用树。根据子节点的数量,二叉树分为三类。

Full Binary Tree

  1. 满二叉树是一种二叉树类型,其中每个节点要么有 0 个,要么有 2 个子节点。

Complete Binary Tree

  1. 完全二叉树是一种二叉树类型,其中所有的叶子节点必须位于同一层级。然而,完全二叉树中的根节点和内部节点可以有 0 个、1 个或 2 个子节点。

Perfect Binary Tree

  1. 完美二叉树是一种二叉树类型,其中所有的叶子节点都位于同一层级,且除叶子节点外的每个节点都有 2 个子节点。

Binary Trees

Binary Search Trees

二叉查找树具有二叉树的所有属性,还包括基于一些约束的额外属性,使其比二叉树更高效。

二叉查找树 (BST) 中的数据总是以这样的方式存储:左子树中的值始终小于根节点中的值,右子树中的值始终大于根节点中的值,即左子树 < 根节点 ≤ 右子树。

binary serach tree

Advantages of BST

  1. 二叉查找树比二叉树更有效率,因为执行各种操作的时间复杂度降低了。

  2. 由于键的顺序仅基于父节点,因此搜索操作变得更简单。

  3. BST 的对齐方式也支持范围查询,该查询用于查找存在于两个键之间的值。这有助于数据库管理系统。

Disadvantages of BST

二叉查找树的主要缺点是,如果节点中的所有元素都大于或小于根节点,* 则树会变得不平衡 *。简而言之,树会完全向一侧倾斜。

skewness 会使树成为链表而不是 BST,因为搜索操作的最坏情况时间复杂度变为 O(n)。

为了克服二叉查找树中的这种不平衡问题,引入了 Balanced Binary Search Trees 的概念。

Balanced Binary Search Trees

考虑一棵二叉查找树,左子树的高度为“m”,右子树的高度为“n”。如果 (m-n) 的值等于 0、1 或 -1,则该树被称为 Balanced Binary Search Tree

这些树的设计方式是,一旦高度差超过 1,它们就会进行自我平衡。二叉查找树使用旋转作为自我平衡算法。有四种不同类型的旋转:左左旋转、右右旋转、左右旋转和右左旋转。

有各种类型的自平衡二叉查找树 -

  1. AVL Trees

  2. Red Black Trees

  3. B Trees

  4. B+ Trees

  5. Splay Trees

  6. Priority Search Trees

Tree Traversal

遍历是一个访问树的所有节点并可能打印其值的过程。因为,所有节点都通过边(链接)连接,所以我们总是从根(头)节点开始。也就是说,我们无法随机访问树中的节点。有三种方法可以遍历树 -

  1. In-order Traversal

  2. Pre-order Traversal

  3. Post-order Traversal

通常,我们遍历一棵树以搜索或定位树中给定的项目或键,或打印它包含的所有值。

In-order Traversal

在这种遍历方法中,首先访问左子树,然后访问根,最后访问右子树。我们应该始终记住,每个节点本身都可能表示一个子树。

如果遍历一棵二叉树 in-order ,输出将按升序生成排序的键值。

In order Traversal

A 开始,按照中序遍历,我们将移动到它的左子树 BB 也以中序遍历。此过程一直持续到所有节点都已访问。这棵树的中序遍历的输出将是−

D → B → E → A → F → C → G

Algorithm

到所有节点都已遍历−

Step 1 − 递归遍历左子树。

Step 2 − 访问根节点。

Step 3 − 递归遍历右子树。

Example

以下是该操作在各种编程语言中的实现 −

Pre-order Traversal

在这种遍历方法中,首先访问根节点,然后访问左子树,最后访问右子树。

Pre order Traversal

A 开始,并按照先序遍历,我们首先访问 A 本身,然后移动到它的左子树 BB 也以先序遍历。此过程一直持续到所有节点都已访问。这棵树的先序遍历的输出将是−

A → B → D → E → C → F → G

Algorithm

到所有节点都已遍历−

Step 1 − 访问根节点。

Step 2 − 递归遍历左子树。

Step 3 − 递归遍历右子树。

Example

以下是该操作在各种编程语言中的实现 −

Post-order Traversal

在这种遍历方法中,根节点最后被访问,因此得名。我们首先遍历左子树,然后遍历右子树,最后遍历根节点。

Post order Traversal

A 开始,并按照后序遍历,我们首先访问左子树 BB 也以后序遍历。此过程一直持续到所有节点都已访问。这棵树的后序遍历的输出将是−

D → E → B → F → G → C → A

Algorithm

到所有节点都已遍历−

Step 1 − 递归遍历左子树。

Step 2 − 递归遍历右子树。

Step 3 − 访问根节点。

Example

以下是该操作在各种编程语言中的实现 −

要检查树遍历的 C 实现,请 click here

Implementation

遍历是一个访问树的所有节点并可能打印其值的过程。因为,所有节点都通过边(链接)连接,所以我们总是从根(头)节点开始。也就是说,我们无法随机访问树中的节点。有三种方法可以遍历树 -

  1. In-order Traversal

  2. Pre-order Traversal

  3. Post-order Traversal

我们现在将在这里使用以下二叉树,看到树遍历在 C 编程语言中的实现−

tree traversal

Example

以下是该操作在各种编程语言中的实现 −

Binary Search Tree

二叉查找树(BST)是其中所有节点都符合以下所述属性的树——

  1. 节点的左子树的键小于或等于其父节点的键。

  2. 节点的右子树的键大于或等于其父节点的键。

因此,BST 将其所有子树划分为两个部分;左子树和右子树,并且可以定义为——

left_subtree (keys) ≤ node (key) ≤ right_subtree (keys)

Representation

BST 是按照可以维持 BST 属性的方式排列的一组节点。每个节点都有一个键和一个相关值。在搜索时,将所需的键与 BST 中的键比较,如果找到,则检索相关值。

以下是 BST 的图像表示——

tree traversal

我们观察到,根节点键(27)在左子树上具有所有值较小的键,在右子树上具有值较大的键。

Basic Operations

以下是树的基本操作——

  1. Search - 在树中搜索一个元素。

  2. Insert - 在树中插入一个元素。

  3. Pre-order Traversal - 以先序方式遍历树。

  4. In-order Traversal - 以中序方式遍历树。

  5. Post-order Traversal - 以后序方式遍历树。

Defining a Node

定义一个节点,该节点存储一些数据以及对其左子节点和右子节点的引用。

struct node {
   int data;
   struct node *leftChild;
   struct node *rightChild;
};

Search Operation

每当要搜索一个元素时,从根节点开始搜索。然后,如果数据小于键值,则在左子树中搜索元素。否则,在右子树中搜索该元素。对每个节点遵循相同的算法。

Algorithm

1. START
2. Check whether the tree is empty or not
3. If the tree is empty, search is not possible
4. Otherwise, first search the root of the tree.
5. If the key does not match with the value in the root, search its subtrees.
6. If the value of the key is less than the root value, search the left subtree
7. If the value of the key is greater than the root value, search the right subtree.
8. If the key is not found in the tree, return unsuccessful search.
9. END

Example

以下是该操作在各种编程语言中的实现 −

Insert Operation

每当要插入一个元素时,首先找到它的适当位置。从根节点开始搜索,然后,如果数据小于键值,则在左子树中搜索空位置并插入该数据。否则,在右子树中搜索空位置并插入该数据。

Algorithm

1 – START
2 – If the tree is empty, insert the first element as the root node of the tree. The following elements are added as the leaf nodes.
3 – If an element is less than the root value, it is added into the left subtree as a leaf node.
4 – If an element is greater than the root value, it is added into the right subtree as a leaf node.
5 – The final leaf nodes of the tree point to NULL values as their child nodes.
6 – END

Example

以下是该操作在各种编程语言中的实现 −

Inorder Traversal

二叉查找树中的中序遍历操作按照以下顺序访问其所有节点——

  1. 首先,遍历根节点/当前节点的左子节点(如果存在)。

  2. 接下来,遍历当前节点。

  3. 最后,如果存在,遍历当前节点的右侧子树。

Algorithm

1. START
2. Traverse the left subtree, recursively
3. Then, traverse the root node
4. Traverse the right subtree, recursively.
5. END

Example

以下是该操作在各种编程语言中的实现 −

Preorder Traversal

二叉搜索树的先序遍历操作将访问其所有节点。但是,首先打印根节点,然后是其左子树,最后是其右子树。

Algorithm

1. START
2. Traverse the root node first.
3. Then traverse the left subtree, recursively
4. Later, traverse the right subtree, recursively.
5. END

Example

以下是该操作在各种编程语言中的实现 −

Postorder Traversal

与其他遍历类似,后序遍历也会访问二叉搜索树中的所有节点并显示它们。但是,首先打印左子树,然后是右子树,最后是根节点。

Algorithm

1. START
2. Traverse the left subtree, recursively
3. Traverse the right subtree, recursively.
4. Then, traverse the root node
5. END

Example

以下是该操作在各种编程语言中的实现 −

Example

以下是该操作在各种编程语言中的实现 −

AVL Trees

要发明的第一种自平衡二叉搜索树是 AVL 树。AVL 树的名称是由其发明者的名字命名的 − Adelson-Velsky 和 Landis。

在 AVL 树中,左右子树的高度差,称为 Balance Factor ,最多必须为 1。一旦差值超过 1,该树会自动执行平衡算法,直至差值再次变为 1。

BALANCE FACTOR = HEIGHT(LEFT SUBTREE) – HEIGHT(RIGHT SUBTREE)

在 AVL 树的平衡算法中通常有四种旋转情况:LL、RR、LR、RL。

LL Rotations

当将节点插入导致不平衡树的右子树时,执行 LL 旋转。这是一个单一的左旋转,再次使树平衡 −

LL Rotations

Fig : LL Rotation

发生不平衡的节点变为左子节点,新添加的节点变为右子节点,中间节点变为父节点。

RR Rotations

当将节点插入导致不平衡树的左子树时,执行 RR 旋转。这是一个单一的右旋转,再次使树平衡 −

RR Rotations

Fig : RR Rotation

发生不平衡的节点变为右子节点,新添加的节点变为左子节点,中间节点变为父节点。

LR Rotations

LR 旋转是之前单一旋转的扩展版本,也称为双旋转。当将节点插入左子树的右子树中时,执行此操作。LR 旋转将左旋转与右旋转相结合。有几个需要遵循的步骤来执行此操作。

  1. 考虑一个示例,其中“A”为根节点,“B”为“A”的左子节点,“C”为“B”的右子节点。

  2. 由于不平衡发生在 A 处,因此对 A 的子节点 B 和 C 应用左旋转。

  3. 旋转后,C 节点变为 A 的左子节点,B 变为 C 的左子节点。

  4. 不平衡仍然存在,因此在根节点 A 和左子节点 C 处应用右旋转。

  5. 经过最终的右旋转,C 变为根节点,A 变为右子节点,B 为左子节点。

LR Rotation

Fig : LR Rotation

RL Rotations

RL 旋转也是先前单旋转的扩展版本,因此它被称为双旋转,并且当将一个节点插入右子树的左子树中时执行该旋转。RL 旋转是右旋转和左旋转的组合。需要遵循多个步骤来执行此操作。

  1. 考虑一个示例,“A”作为根节点,“B”作为“A”的右子节点,“C”作为“B”的左子节点。

  2. 由于不平衡发生在 A 处,因此对 A 的子节点(即 B 和 C)应用右旋转。

  3. 旋转后,C 节点变成 A 的右子节点,B 变成 C 的右子节点。

  4. 不平衡仍然存在,因此在根节点 A 和右子节点 C 处应用左旋转。

  5. 在最后一次左旋转之后,C 成为根节点,A 成为左子节点,B 成为右子节点。

RL Rotations

Fig : RL Rotation

Basic Operations of AVL Trees

对 AVL 树结构执行的基本操作包括对二叉查找树执行的所有操作,因为 AVL 树的核心实际上只是一个保存其所有属性的二叉查找树。因此,对 AVL 树执行的基本操作为 InsertionDeletion

Insertion

通过遵循插入的二叉查找树属性将数据插入 AVL 树,即左子树必须包含小于根值的值,右子树必须包含所有更大的值。然而,在 AVL 树中,在插入每个元素后,都会检查树的平衡因子;如果它不超过 1,则树保持原样。但是,如果平衡因子超过 1,则应用平衡算法来调整树,使得平衡因子再次小于或等于 1。

Algorithm

AVL 树的插入操作涉及以下步骤 -

Step 1 − 创建一个节点

Step 2 − 检查树是否为空

Step 3 − 如果树为空,则创建的新节点将成为 AVL 树的根节点。

Step 4 − 如果树不为空,则执行二叉查找树插入操作,并检查树中节点的平衡因子。

Step 5 − 假设平衡因子超过 ±1,我们对所述节点应用合适的旋转,并从第 4 步继续插入。

START
   if node == null then:
      return new node
   if key < node.key then:
      node.left = insert (node.left, key)
   else if (key > node.key) then:
      node.right = insert (node.right, key)
   else
      return node
   node.height = 1 + max (height (node.left), height (node.right))
   balance = getBalance (node)
   if balance > 1 and key < node.left.key then:
      rightRotate
   if balance < -1 and key > node.right.key then:
      leftRotate
   if balance > 1 and key > node.left.key then:
      node.left = leftRotate (node.left)
      rightRotate
   if balance < -1 and key < node.right.key then:
      node.right = rightRotate (node.right)
      leftRotate (node)
   return node
END

Insertion Example

让我们通过构建一个包含 1 到 7 个整数的示例 AVL 树来理解插入操作。

从第一个元素 1 开始,我们创建一个节点并测量平衡,即 0。

AVL1

由于二叉查找属性和平衡因子都得到满足,我们再向树中插入另一个元素。

AVL2

计算两个节点的平衡因子,发现为 -1(左子树的高度为 0,右子树的高度为 1)。由于它没有超过 1,我们向树中添加另一个元素。

AVL3

现在,添加第三个元素后,平衡因子超过 1 并变成 2。因此,应用旋转。由于不平衡发生在两个右侧节点上,所以在这种情况下应用 RR 旋转。

AVL4

树被重新排列为 −

AVL5

类似地,使用这些旋转插入并重新排列下一个元素。重新排列后,我们实现树为 −

AVL6

Example

以下是该操作在各种编程语言中的实现 −

Deletion

AVL 树中的删除发生在三种不同的情况下 −

  1. Scenario 1 (Deletion of a leaf node) − 如果要删除的节点是叶节点,则删除它而不进行任何替换,因为它不会干扰二叉搜索树属性。然而,平衡因子可能会被破坏,因此应用旋转来恢复它。

  2. Scenario 2 (Deletion of a node with one child) − 如果要删除的节点有一个子节点,则用其子节点中的值替换该节点中的值。然后删除子节点。如果平衡因子被破坏,则应用旋转。

  3. Scenario 3 (Deletion of a node with two child nodes) − 如果要删除的节点有两个子节点,则找到该节点的中序后继并用中序后继值替换其值。然后尝试删除中序后继节点。如果删除后平衡因子超过 1,则应用平衡算法。

START
   if root == null: return root
   if key < root.key:
      root.left = delete Node
   else if key > root.key:
      root.right = delete Node
   else:
      if root.left == null or root.right == null then:
         Node temp = null
         if (temp == root.left)
            temp = root.right
         else
            temp = root.left
         if temp == null then:
            temp = root
            root = null
         else
            root = temp
         else:
            temp = minimum valued node
            root.key = temp.key
            root.right = delete Node
      if (root == null) then:
         return root
         root.height = max (height (root.left), height (root.right)) + 1
         balance = getBalance
      if balance > 1 and getBalance (root.left) >= 0:
         rightRotate
      if balance > 1 and getBalance (root.left) < 0:
         root.left = leftRotate (root.left);
         rightRotate
      if balance < -1 and getBalance (root.right) <= 0:
         leftRotate
      if balance < -1 and getBalance (root.right) > 0:
         root.right = rightRotate (root.right);
         leftRotate
      return root
END

Deletion Example

使用上面给出的同一棵树,让我们在三种情况下执行删除 −

AVL7
  1. 从上面这棵树中删除元素 7 −

由于元素 7 是一个叶节点,我们通常在不干扰树中的任何其他节点的情况下删除该元素

AVL8
  1. 从获得的输出树中删除元素 6 −

然而,元素 6 不是一个叶节点,并且有一个子节点与之相连。在这种情况下,我们用其子节点替换节点 6:节点 5。

AVL9

树的平衡变为 1,并且因为它没有超过 1,所以树保持原样。如果我们进一步删除元素 5,我们不得不应用左旋转;由于不平衡发生在 1-2-4 和 3-2-4,所以可能是 LL 或 LR。

AVL10

删除元素 5 后平衡因子受到干扰,因此我们应用 LL 旋转(这里我们也可以应用 LR 旋转)。

AVL11

一旦在路径 1-2-4 上应用 LL 旋转,节点 3 保持为节点 2 的右子节点(现在由节点 4 占据)的样子。因此,该节点被添加到节点 2 的右子树中,并作为节点 4 的左子节点。

AVL12
  1. 从剩余的树中删除元素 2 −

如场景 3 中所提到的那样,该节点有两个子节点。因此,我们找到它是一个叶节点的中序后继(例如,3)并用中序后继替换其值。

AVL13

树的平衡仍然保持为 1,因此我们在不进行任何旋转的情况下将树保留原样。

Example

以下是该操作在各种编程语言中的实现 −

Implementation of AVL Trees

在以下实现中,我们按升序考虑输入,并通过计算平衡因子和应用旋转将它们存储在 AVL 树中。

Example

以下是该操作在各种编程语言中的实现 −

Red Black Trees

Red-Black 树是另一种类型的平衡二叉查找树,带有两种颜色的节点:红色和黑色。这是一棵自平衡二叉查找树,它利用这些颜色来在插入和删除操作期间维持平衡因子。因此,在 Red-Black 树操作期间,内存会使用 1 位存储来容纳每个节点的颜色信息。

在 Red-Black 树(也称为 RB 树)中,在为节点分配颜色时有一些不同的条件需要遵循。

  1. 根节点始终为黑色。

  2. 任意的两个相邻节点都不得为红色。

  3. 树中的每条路径(从根节点到叶节点)都必须包含相同数量的黑色节点。

尽管 AVL 树比 RB 树更平衡(AVL 树中的平衡算法比 RB 树中的严格),但通过 RB 树可以更有效地进行多次且快速的插入和删除操作。

RB trees

Fig: RB trees

Basic Operations of Red-Black Trees

对 Red-Black 树的操作包括通常在二叉查找树上执行的所有基本操作。RB 树的一些基本操作包括:

  1. Insertion

  2. Deletion

  3. Search

Red-Black 树的插入操作遵循二叉查找树的相同插入算法。该元素按照二叉查找属性插入,另外,对节点进行红黑颜色编码以根据红黑树属性对树进行平衡。

按照以下给定的程序将元素插入红黑树,同时维护二叉查找树和红黑树属性。

Case 1 - 检查树是否为空;如果树为空,则将当前节点作为根,并将该节点着色为黑色。

Case 2 - 但如果树不为空,则创建一个新节点并将其着色为红色。此处我们面临两种不同的情况:

  1. 如果新节点的父级为黑色节点,则退出操作,树保持原样。

  2. 如果此新节点的父级为红色,并且父级的兄弟节点的顏色为黑色或不存在,则应用适当的旋转并相应地重新着色。

  3. 如果此新节点的父级为红色,并且父级的兄弟节点的颜色为红色,则将父级、兄弟节点和祖先节点重新着色为黑色。仅当祖先节点是 not 根节点时才会重新着色;如果是根节点,则仅重新着色父级和兄弟节点。

Insertion Example

让我们构造一棵 RB 树以详细了解插入操作的前 7 个整数:

检查树是否为空,因此添加的第一个节点是根,并将其着色为黑色。

first node

现在,树不为空,因此我们创建一个新节点,并用红色添加下一个整数,

new node

节点不违反二叉查找树和 RB 树属性,因此我们继续添加另一个节点。

树不为空;我们用下一个整数创建一个新的红色节点。但新节点的父节点不是黑色节点,

third node

现在树违反了二叉搜索树和 RB 树的属性;因为父节点的兄弟节点是 NULL,我们应用了合适的旋转并重新给节点着色。

suitable rotation

现在 RB 树属性已恢复,我们向树中添加另一个节点 −

RB Tree property

树再次违背 RB 树的平衡属性,因此我们检查父节点的兄弟节点颜色,在本例中为红色,因此我们只需重新给父节点和兄弟节点着色。

RB Tree balance property

接下来,我们插入元素 5,这使树再次违反 RB 树的平衡属性。

insert element 5

并且由于兄弟节点是 NULL,我们应用了合适的旋转并重新给颜色。

sibling is NULL

现在,我们插入元素 6,但 RB 树属性被违反,需要应用插入案例之一 −

insert element 6

父节点的兄弟节点是红色的,因此我们重新给父节点、父节点的兄弟节点和祖父节点着色,因为祖父节点不是根节点。

recolor parent

现在,我们添加最后一个元素 7,但这个新节点的父节点是红色的。

add last element

因为父节点的兄弟节点是 NULL,我们应用合适的旋转(RR 旋转)

RB Tree achieved

最终的 RB 树已完成。

Deletion

红黑树上的删除操作必须以一种方式执行,这种方式必须恢复二叉搜索树和红黑树的所有属性。按照以下步骤在红黑树上执行删除操作 −

首先,我们基于二叉搜索树属性执行删除。

Case 1 - 如果要删除的节点或节点的父节点为红色,则直接删除它。

Case 2 - 如果节点是双黑色,则只需删除双黑色(双黑色发生在待删除节点为黑色叶节点时,因为它添加了也被视为黑色节点的 NULL 节点)

Case 3 - 如果双黑节点的兄弟节点也是黑色节点并且其子节点也是黑色,请按照以下步骤操作 −

  1. Remove double black

  2. 将其父节点重新着色为黑色(如果父节点为红色节点,它将变为黑色;如果父节点已经是黑色节点,它将变成双黑色)

  3. 用红色重新给父节点的兄弟节点着色

  4. 如果双黑节点仍然存在,我们应用其他情况。

Case 4 − 如果双黑节点的兄弟节点为红色,则执行以下操作 −

  1. 交换父节点和父节点兄弟节点的颜色。

  2. 朝着双黑节点的方向旋转父节点

  3. 重新应用合适的情况。

Case 5 − 如果双黑节点的兄弟节点是黑节点,但兄弟节点的一个最近的子节点为红色,则按照以下步骤进行 −

  1. 交换双黑节点的兄弟节点和相应子节点的颜色。

  2. 旋转兄弟节点和双黑节点在相反的方向(也就是说,如果双黑节点是右子节点,则进行左旋转,反之亦然)

  3. Apply case 6.

Case 6 − 如果双黑节点的兄弟节点是黑节点,但兄弟节点的一个更远的子节点为红色,则按照以下步骤进行 −

  1. 交换双黑节点的父节点和兄弟节点的颜色。

  2. 朝着双黑节点的方向旋转父节点(也就是说,如果双黑节点是右子节点,则进行右旋转,反之亦然)

  3. Remove double black

  4. 将红色子节点的颜色更改为黑色。

Deletion Example

考虑以上构建的红黑树,让我们从树中删除一些元素。

RB Tree achieved

从树中删除元素 4、5、3。

要删除元素 4,让我们首先执行二元查找删除。

delete element 4

执行二元查找删除后,红黑树属性不受干扰,因此树保持原样。

然后,我们使用二元查找删除来删除元素 5

delete element 5

但是在执行二元查找删除后,红黑属性被破坏,即树中的所有路径未保存相同数量的黑节点;因此,我们交换颜色来平衡树。

RB property violated

然后,我们从获得的树中删除节点 3 −

应用二进制查找删除,我们正常删除节点 3,因为它是一个叶节点。由于 3 是一个黑色节点,我们得到了一个双重节点。

delete node 3

我们应用情况 3 删除,因为双重黑色的兄弟节点是黑色,其子节点也是黑色。在这里,我们删除双重黑色,为双重黑色的父级和兄弟节点重新着色。

RB Tree property maintained

所有所需的节点都被删除,并且 RB 树属性得到了保持。

红黑树中的搜索操作遵循与二叉查找树相同的算法。遍历树,并比较每个节点与要搜索的关键元素;如果找到,则返回成功的搜索。否则,返回不成功的搜索。

Complete implementation

B Trees

B 树是扩展的二叉查找树,专门用于 m 路搜索,因为 B 树的阶为“m”。树的阶定义为一个节点可容纳的最大子节点数。因此,B 树的高度比 AVL 树和 RB 树的高度要低得多。

它们是二分搜索树的一般形式,因为它包含多个键和两个子项。

B 树的各种属性包括 -

  1. B 树中的每个节点最多容纳 m 个子项和 (m-1) 个键,因为树的阶为 m。

  2. B 树中的每个节点(除根节点和叶节点外)至少可以容纳 m/2 个子节点

  3. 根节点必须至少有两个子节点。

  4. B 树中的所有路径都必须在同一级别结束,即叶子节点必须在同一级别。

  5. B 树始终维护排序后的数据。

b trees

B 树还广泛用于磁盘访问,因为它高度较低,减少了磁盘访问时间。

Note - 磁盘访问是计算机磁盘的内存访问,信息存储在其中,磁盘访问时间是系统访问磁盘内存所花费的时间。

Basic Operations of B Trees

B 树中支持的操作为插入、删除和搜索,每个操作的时间复杂度为 O(log n)

Insertion

B 树的插入操作类似于二叉搜索树,但将元素插入相同节点中,直至达到最大键。使用以下过程执行插入:

Step 1 − 计算一个节点可以容纳的最大 $\mathrm{\left ( m-1 \right )}$ 和最小 $\mathrm{\left ( \left \lceil \frac{m}{2}\right \rceil-1 \right )}$ 数目的键,其中 m 表示 B 树的阶。

Calculate min max

Step 2 - 使用二进制搜索插入将数据插入到树中,一旦键达到最大数量,节点将分成两半,中键成为内部节点,而左右键成为其子节点。

data inserted

Step 3 - 所有叶子节点必须在同一层。

leaf nodes same level

键 5、3、21、9、13 根据二进制搜索属性全部添加到节点中,但如果我们添加键 22,则它将违反最大键属性。因此,节点分成两半,中键移动到父节点,然后继续插入。

adding 11

在插入 11 时也会出现另一次小故障,因此节点被分成两半,中键移动到父级。

adding 16

在插入 16 时,即使将节点分成两部分,父节点也溢出,因为其达到最大键数。因此,先拆分父节点,中键成为根。然后,将叶节点分成两半,叶节点的中键移动到其父级。

final B tree

插入所有元素后,将实现最终的 B 树。

Example

// Insert the value
void insertion(int item) {
   int flag, i;
   struct btreeNode *child;
   flag = setNodeValue(item, &i, root, &child);
   if (flag)
      root = createNode(i, child);
}

Deletion

B 树中的删除操作与二叉搜索树的删除操作略有不同。从 B 树中删除节点的过程如下:

Case 1 - 如果要删除的键位于叶节点中并且删除不违反最小键属性,则只需删除该节点。

delete key 14
deleted key 14

Case 2 - 如果要删除的键位于叶节点中,但删除违反了最小键属性,则向其左兄弟节点或右兄弟节点借一个键。如果两个兄弟节点都具有相同数量的最小键,则将节点合并到其中任何一个中。

delete key 3
deleted key 3

Case 3 - 如果要删除的键位于内部节点中,则用其左子节点或右子节点中的键替换该键,具体取决于哪个子节点具有更多的键。但是,如果两个子节点都具有最小数量的键,则它们会被合并在一起。

delete key 13
deleted key 13

Case 4 - 如果要删除的键位于内部节点中,违反了最小键属性,并且其子节点和兄弟节点都具有最小数量的键,则合并子节点。然后,将其兄弟节点与父节点合并。

delete key 5
deleted key 5

下面是 B 树中删除操作的 C++ 功能代码片段 −

// Deletion operation
void deletion(int key){
   int index = searchkey(key);
   if (index < n && keys[index] == key) {
      if (leaf)
         deletion_at_leaf(index);
      else
         deletion_at_nonleaf(index);
   } else {
      if (leaf) {
         cout << "key " << key << " does not exist in the tree\n";
         return;
      }
      bool flag = ((index == n) ? true : false);
      if (C[index]->n < t)
         fill(index);
      if (flag && index > n)
         C[index - 1]->deletion(key);
      else
         C[index]->deletion(key);
   }
   return;
}

// Deletion at the leaf nodes
void deletion_at_leaf(int index){
   for (int i = index + 1; i < n; ++i)
      keys[i - 1] = keys[i];
   n--;
   return;
}

// Deletion at the non leaf node
void deletion_at_nonleaf(int index){
   int key = keys[index];
   if (C[index]->n >= t) {
      int pred = get_Predecessor(index);
      keys[index] = pred;
      C[index]->deletion(pred);
   } else if (C[index + 1]->n >= t) {
      int successor = copysuccessoressor(index);
      keys[index] = successor;
      C[index + 1]->deletion(successor);
   } else {
      merge(index);
      C[index]->deletion(key);
   }
   return;
}

Example

B+ Trees

B+ 树是 B 树的扩展,旨在使插入、删除和搜索操作更有效率。

B+ 树的性质与 B 树的性质相似,不同之处在于 B 树可以在所有内部节点和叶子节点中存储键和记录,而 B+ 树则在叶子节点中存储记录,在内部节点中存储键。B+ 树的一个显著特性是,所有叶子节点都以单链表格式彼此连接,并且提供了一个数据指针来指向磁盘文件中存在的数据。这有助于以相等数量的磁盘访问来获取记录。

由于主内存的大小有限,因此 B+ 树充当了存储在主内存中无法存储的记录的数据存储。为此,内部节点存储在主内存中,而叶子节点存储在辅助内存存储中。

bplus trees

Properties of B+ trees

  1. B+ 树中的每个节点,根节点除外,最多容纳 m 个子节点和 (m-1) 个键,最少容纳 $\mathrm{\left \lceil \frac{m}{2}\right \rceil}$ 个子节点和 $\mathrm{\left \lceil \frac{m-1}{2}\right \rceil}$ 个键,因为树的阶数为 m

  2. 根节点必须至少有两个子节点和至少一个搜索键。

  3. B 树中的所有路径都必须在同一级别结束,即叶子节点必须在同一级别。

  4. B+ 树始终维护已排序数据。

Basic Operations of B+ Trees

B+ 树支持的操作是插入、删除和搜索,所有操作的时间复杂度为 O(log n)

它们与 B 树操作类似,因为将数据存储在此两种数据结构中的基本思想是相同的。然而,两者的不同之处在于,与 B 树不同,数据仅存储在 B+ 树的叶结点中。

Insertion

B+ 树的插入从叶结点开始。

Step 1 − 计算要添加到 B+ 树结点上的最大和最小键数。

calculate number of keys

Step 2 − 将元素一个接一个地插入叶结点,直到超过最大键数。

Insert elements

Step 3 − 将结点分成两半,其中左子结点包含最小键数,而剩余键存储在右子结点中。

node split

Step 4 − 但是,如果内部结点也超过了最大键属性,则将结点分成两半,其中左子结点包含最小键,而剩余键存储在右子结点中。但是,将右子结点中最小的数字设为父结点。

insert into b plus tree

Step 5 − 如果叶结点和内部结点都具有最大键,则它们都会以类似的方式分割,并且右子结点中的最小键添加到父结点中。

b plus tree order 4

Deletion

在 B+ 树中的删除操作中,我们需要考虑数据结构中的冗余。

Case 1 − 对于存在于超过最小键数目的叶节点且副本不存在于内部节点中的键,简单地删除它。

delete 7
deleted 7

Case 2 − 对于存在于仅包含最小键数目的叶节点且副本不存在于内部节点中的键,从其兄弟节点借用一个键并删除所需的键。

Case 3 − 对于存在于叶节点且副本存在于内部节点中的键,有多种可能的情况 −

  1. 叶节点和内部节点中都存在超过最小键数目的情况下:简单地删除该键,并且仅将中序后继添加到内部节点。

delete 5
deleted 5
  1. 叶节点中存在最小键数目:删除该节点,并且从其最近的兄弟节点借用一个节点,并且将其值也替换为内部节点。

delete 4
deleted 4
  1. 如果要删除的叶节点副本在其祖先节点中,则删除该节点并删除该空位。该祖先节点被填入已删除节点的中序后继。

delete 3

Case 4 − 如果要删除的键存在于最小键属性中的一个违反规则的节点中,其父级和兄弟节点都包含最小键数目,则删除该键,并且将其兄弟节点与其父级合并。

deleted 3

Example

Splay Trees

Splay 树是二叉搜索树的变体,因为它包含所有 BST 操作,如插入、删除和搜索,然后是另一个称为 splaying 的扩展操作。

例如,应该将一个值为 “A” 的值插入到树中。如果树是空,则将 “A” 添加到树的根节点并退出;但是,如果树不为空,则使用二进制查找插入操作来插入元素,然后在新节点上执行伸展操作。

类似地,在 splay 树中搜索一个元素之后,包含该元素的节点也必须被 splaying。

但是我们如何执行 splaying?简单来说,splaying 就是将一个操作节点移到根处的过程。它有六种类型的旋转。

  1. Zig rotation

  2. Zag rotation

  3. Zig-Zig rotation

  4. Zag-Zag rotation

  5. Zig-Zag rotation

  6. Zag-Zig rotation

Zig rotation

当操作节点是根节点或根节点的左子节点时,执行 zig 旋转。该节点向右旋转。

Zig rotation

经过移动后,树看起来如下:

after shift

Zag rotation

当操作节点是根节点或根节点的右子节点时,也执行 zag 旋转。该节点向左旋转。

Zag rotation

经过移动后,操作节点变为根节点:

root node

Zig-Zig rotation

当操作节点同时具有父节点和祖先节点时,执行 zig-zig 旋转。该节点向右移动两个位置。

Zig Zig rotation

第一次旋转将树向右移动一个位置:

root node 5

第二次向右旋转将再次将节点移动一个位置。移动后的最终树看起来如下:

root node 3

Zag-Zag rotation

当操作节点同时具有父节点和祖先节点时,也执行 zag-zag 旋转。该节点向左移动两个位置。

Zag Zag rotation

经过第一次旋转后,树看起来如下:

after first rotation

然后,第二次旋转后的最终树如下所示。然而,操作节点仍然不是根,因此 splaying 被认为不完整。因此,在这种情况下再次应用其他合适的旋转,直到该节点成为根节点。

after second rotatios

Zig-Zag rotation

当操作节点同时具有父节点和祖先节点时,执行 zig-zag 旋转。但不同之处在于,祖先节点父节点和子节点采用 LRL 格式。该节点先向右旋转,然后向左旋转。

Zig Zag rotation

经过第一次旋转后,树如下:

left rotation

经过第二次旋转后的最终树如下:

root node 8

Zag-Zig rotation

当操作节点同时具有父节点和祖先节点时,也执行 zag-zig 旋转。但不同之处在于,祖先节点、父节点和子节点采用 RLR 格式。该节点先向左旋转,然后向右旋转。

Zag Zig rotation

进行了第一次旋转,得到如下树:

tree obtained

经过第二次旋转,最终得到如下树。然而,操作节点尚未成为根节点,因此需要执行另一次旋转,将该节点设为根。

after second rotation

Basic Operations of Splay Trees

伸展树包含二叉搜索树提供的所有基本操作:插入、删除和搜索。然而,在每个操作之后,都会有一个附加操作将其与二叉搜索树操作区分开来:伸展。我们已经学过伸展,因此,让我们理解其他操作的过程。

Insertion

伸展树中的插入操作与在二叉搜索树中执行插入操作的方式完全相同。在伸展树中执行插入操作的过程如下:

  1. 检查树是否为空;如果为空,则添加新节点并退出

insertion
  1. 如果树不为空,则使用二叉搜索插入将新节点添加到现有树中。

adding nodes
  1. 然后,选择并应用合适类型的伸展到新添加的节点。

splaying chosen

Zag (Left) Rotation is applied on the new node

left rotation applied

Example

以下是该操作在各种编程语言中的实现 −

Deletion

伸展树中的删除操作执行如下:

  1. 对要删除的节点应用伸展操作。

  2. 节点成为根后,删除该节点。

  3. 现在,树被分成两棵树,左子树和右子树;其各自的第一个节点为根节点:例如,root_left 和 root_right。

delete
  1. 如果 root_left 为 NULL 值,则 root_right 将成为树的根。反之亦然。

  2. 但如果 root_left 和 root_right 都不是 NULL 值,则从左子树中选择最大值并通过连接子树将其设为新根。

deleted 5 node

Example

以下是该操作在各种编程语言中的实现 −

Search

伸展树中的搜索操作遵循二叉搜索树操作的相同过程。然而,在进行搜索并找到元素之后,对搜索的节点应用伸展。如果找不到元素,则提示搜索不成功。

Example

以下是该操作在各种编程语言中的实现 −

Tries Data Structure

trie 是一种多路搜索树,它根本用于从字符串或一组字符串中检索特定的键。它以一种有效的方式存储数据,因为它对字母表中的每个字母都使用指针。

trie 数据结构基于字符串的公共前缀。考虑到集中字符串的数量,根节点可以具有任意数目的节点。trie 的根节点不包含任何值,只包含其子节点的指针。

有三种类型的 trie 数据结构 −

  1. Standard Tries

  2. Compressed Tries

  3. Suffix Tries

trie 的实际应用包括:自动更正、文本预测、情感分析和数据科学。

trie

Basic Operations in Tries

trie 数据结构还执行树数据结构执行的相同操作。它们是:

  1. Insertion

  2. Deletion

  3. Search

Insertion

trie 中的插入操作是一种简单的方法。trie 中的根不包含任何值,并且插入操作从根的直接子节点开始,这些子节点充当其子节点的键。然而,我们观察到trie中的每个节点都代表输入字符串中的一个字符。因此,字符一个接一个地添加到trie中,而trie中的链接充当指向下一级节点的指针。

Example

Deletion

trie 中的删除操作使用自下而上的方法执行。在trie中搜索元素并进行删除(如果找到)。然而,在执行删除操作时需要记住一些特殊的情况。

Case 1 - 键是唯一的 - 在这种情况下,整个键路径从节点中删除。(唯一键表示没有其他路径从一条路径分支出来)。

Case 2 - 键不是唯一的 - 更新叶节点。例如,如果要删除的键是 see ,但它是另一个键 seethe 的前缀;我们删除 see 并将 t、h 和 e 的布尔值更改为 false。

Case 3 - 要删除的键已经有一个前缀 - 直到前缀的值被删除,并且前缀保留在树中。例如,如果要删除的键是 heart ,但存在另一个键 he ;因此,我们删除 a、r 和 t,直到只剩下 he。

Example

Search

在trie中搜索是一种相当直接的方法。我们只能基于键节点(插入操作开始的节点)向下移动trie的级别。搜索直到路径的末尾。如果找到该元素,则搜索成功;否则,搜索提示不成功。

Example

Heap Data Structure

堆是平衡二叉树数据结构的一个特例,其中将根节点键与其子节点进行比较并相应排列。如果 α 有子节点 β ,则:

key(α) ≥ key(β)

由于父级的值大于子级的值,因此此属性生成 Max Heap 。基于此标准,堆可以分为两种类型:

For Input → 35 33 42 10 14 19 27 44 26 31

Min-Heap - 其中根节点的值小于或等于其任一子节点。

min heap example

Max-Heap - 其中根节点的值大于或等于其任一子节点。

max heap example

两棵树都是使用相同的输入和到达顺序构建的。

Max Heap Construction Algorithm

我们将使用相同的示例来说明如何创建最大堆。创建最小堆的过程类似,但我们追求最小值而不是最大值。

我们将通过一次插入一个元素推导出最大堆的算法。在任何时候,堆都必须保持其属性。在插入时,我们还假设我们将一个节点插入到已经堆化的树中。

Step 1 − Create a new node at the end of heap.
Step 2 − Assign new value to the node.
Step 3 − Compare the value of this child node with its parent.
Step 4 − If value of parent is less than child, then swap them.
Step 5 − Repeat step 3 & 4 until Heap property holds.

Note - 在最小堆构造算法中,我们希望父节点的值小于子节点的值。

让我们通过动画图解来了解最大堆的构造。我们考虑前面使用过的相同输入样本。

max heap animation

Example

Max Heap Deletion Algorithm

让我们推导出一个从最大堆中删除的算法。最大(或最小)堆中的删除始终发生在根处以删除最大(或最小)值。

Step 1 − Remove root node.
Step 2 − Move the last element of last level to root.
Step 3 − Compare the value of this child node with its parent.
Step 4 − If value of parent is less than child, then swap them.
Step 5 − Repeat step 3 & 4 until Heap property holds.
max heap deletion animation

Example

Recursion Algorithms

一些计算机编程语言允许模块或函数调用自身。这种技术称为递归。在递归中,一个函数 α 要么直接调用自身,要么调用一个函数 β 而该函数反过来又调用了原来的函数 α 。函数 α 称为递归函数。

Example − 一个调用自身的函数。

int function(int value) {
   if(value < 1)
      return;
   function(value - 1);

   printf("%d ",value);
}

Example − 一个调用另一个函数(而该函数反过来又调用自身)的函数。

int function1(int value1) {
   if(value1 < 1)
      return;
   function2(value1 - 1);
   printf("%d ",value1);
}
int function2(int value2) {
   function1(value2);
}

Properties

一个递归函数可以像循环一样无限运行。为了避免递归函数无限运行,递归函数必须具有以下两个特性:

  1. Base criteria − 必须至少有一个基本标准或条件,即满足此条件时,函数停止递归调用自身。

  2. Progressive approach − 递归调用应该逐步进行,即每次进行递归调用时,它都更接近基本标准。

Implementation

许多编程语言通过 stacks 实现递归。通常,每当一个函数 ( caller ) 调用另一个函数 ( callee ) 或自身作为被调用方时,调用方函数会将执行控制权转移到被调用方。这个转移过程还可能涉及将一些数据从调用方传递到被调用方。

这意味着,调用方函数必须暂时挂起其执行,并在执行控制权从被调用方函数返回时继续执行。在这里,调用方函数需要从它暂停执行时的执行点准确地开始。它还需要它当时正在处理的确切相同的数据值。为此,将会为调用方函数创建一个激活记录(或堆栈帧)。

activation records

这个激活记录保存有关局部变量、形式参数、返回地址和传递给调用方函数的所有信息。

Analysis of Recursion

有人可能会争论为何使用递归,因为相同的任务可以用迭代来完成。第一个原因是,递归使程序更具有可读性,并且由于采用了最新的增强型 CPU 系统,因此递归比迭代更有效。

Time Complexity

对于迭代,我们采用迭代次数来计算时间复杂度。同样,对于递归,假设一切都是常数,我们尝试找出进行递归调用的次数。对函数进行一次调用为 Ο(1),因此递归调用 (n) 次将使递归函数 Ο(n)。

Space Complexity

空间复杂度是指模块执行所需的额外空间量。对于迭代,编译器几乎不需要任何额外空间。编译器不断更新在迭代中使用的变量值。但是在递归的情况下,系统需要在进行每次递归调用时存储激活记录。因此,人们认为递归函数的空间复杂度可能高于具有迭代的函数。

Example

Tower of Hanoi Using Recursion

河内塔是一个数学谜题,由三根塔(桩)和多个圆环组成,如下图所示 -

tower of hanoi

这些圆环大小不同,并且按升序堆叠,即较小的圆环放在较大的圆环上面。还有其他变种的谜题,其中磁盘的数量不断增加,但塔数保持不变。

Rules

任务是将所有磁盘移动到另一个塔,而不违反排列顺序。解决河内塔的几个规则如下 -

  1. 任何给定时间只能在塔之间移动一个磁盘。

  2. 只能移除“顶部”磁盘。

  3. 没有大磁盘可以放在小磁盘上。

以下是解决带有三个圆盘的河内塔谜题的动画表示。

tower of hanoi

带有 n 个磁盘的河内塔谜题可以在最少 2n−1 步中解决。此演示显示带有 3 个磁盘的谜题已经进行了 23 - 1 = 7 步。

Algorithm

要编写河内塔算法,首先我们需要学习如何解决圆盘更少的问题,例如 → 1 或 2。我们用 sourcedestinationaux 标记三个塔(仅帮助移动磁盘)。如果我们只有一个磁盘,则可以轻松地从源点移动到目标桩。

如果我们有 2 个磁盘 -

  1. 首先,我们将较小的(顶部)磁盘移动到辅助桩。

  2. 然后,我们将较大的(底部)磁盘移动到目标桩。

  3. 最后,我们将较小的磁盘从辅助桩移动到目标桩。

tower of hanoi two disks

所以现在,我们有能力设计一个用于超过两个磁盘的河内塔算法。我们将磁盘堆栈分成两部分。最大的磁盘(第 n 个磁盘)在一部份中,所有其他 (n-1) 个磁盘在第二部分中。

我们的最终目的是将磁盘 n 从源点移动到目标点,然后将所有其他 (n1) 个磁盘放在它上面。我们可以想象以递归方式对所有给定的磁盘集应用相同的操作。

要遵循的步骤如下 -

Step 1 − Move n-1 disks from source to aux
Step 2 − Move nth disk from source to dest
Step 3 − Move n-1 disks from aux to dest

河内塔的递归算法可以驱动如下 -

START
Procedure Hanoi(disk, source, dest, aux)

   IF disk == 1, THEN
      move disk from source to dest
   ELSE
      Hanoi(disk - 1, source, aux, dest)     // Step 1
      move disk from source to dest          // Step 2
      Hanoi(disk - 1, aux, dest, source)     // Step 3
   END IF

END Procedure
STOP

Example

Fibonacci Series Using Recursion

斐波那契数列通过将两个前一个数字相加来生成后续数字。斐波那契数列从两个数字开始 − F0 & F1 。F0 & F1 的初始值可以分别取 0, 1 或 1, 1。

斐波那契数列满足以下条件:

Fn = Fn-1 + Fn-2

因此,一个斐波那契数列看起来可能是这样 −

F8 = 0 1 1 2 3 5 8 13

或者,这样 −

F8 = 1 1 2 3 5 8 13 21

为了说明,F8 的斐波那契显示为 −

fibonacci animation

Fibonacci Iterative Algorithm

首先,我们尝试起草斐波那契数列的迭代算法。

Procedure Fibonacci(n)
   declare f0, f1, fib, loop

   set f0 to 0
   set f1 to 1

   <b>display f0, f1</b>

   for loop ← 1 to n

      fib ← f0 + f1
      f0 ← f1
      f1 ← fib

      <b>display fib</b>
   end for

end procedure

Fibonacci Recursive Algorithm

让我们学习如何创建斐波那契数列的递归算法。递归的基本准则。

START
Procedure Fibonacci(n)
   declare f0, f1, fib, loop

   set f0 to 0
   set f1 to 1

   display f0, f1

   for loop ← 1 to n

      fib ← f0 + f1
      f0 ← f1
      f1 ← fib

      display fib
   end for

END

Example