数据结构1《hello 算法》整理

整理的内容大部分来源于《hello算法》一书,章节的安排也遵循《hello算法》,部分内容来自王道课程,自学整理,侵权请联系。

一、初识算法

1.1 算法定义 

算法:在有限时间内解决特定问题的一组指令或操作步骤,具有以下特性。

        问题是明确的,包含清晰的输入和输出定义。

        具有可行性,能够在有限步骤、时间和内存空间下完成。

        各步骤都有确定的含义,相同的输入和运行条件下,输出始终相同


1.2 数据结构定义

数据结构是计算机中组织和存储数据的方式,具有以下设计目标。

        空间占用尽量减少,节省计算机内存。

        数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。

        提供简洁的数据表示和逻辑信息,以便使得算法高效运行。

数据之间呈现什么样的逻辑结构,针对这种结构会用到哪些常见的基本运算、基本操作,自己定义出这些操作,在使用计算机代码实现的时候,就需要考虑这些数据存储结构。


1.3 数据结构和算法的关系

 数据结构与算法高度相关、紧密结合,具体表现以下三个方面。

        数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及用于操作数据的方法。

        算法是数据结构发挥作用的舞台。数据结构本身仅存储数据信息,结合算法才能解决特定问题。

        算法通常可以基于不同的数据结构进行实现,并往往有对应最优的数据结构,但最终执行效率可能相差很大。

如果把数据结构和算法类比为积木,此外数据结构和算法独立于编程语言。


二、复杂度分析

2.1 算法效率评估

在算法设计中,我们先后追求以下两个层面的目标。

        找到问题解法:算法需要在规定的输入范围内,可靠地求得问题的正确解。

        寻求最优解法:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。

        在能够解决问题的前提下,可以从下面两个方面对算法的效率进行分析

                时间效率:算法运行速度的快慢。

                空间效率:算法占用内存空间的大小。

        我们应该以设计“既快又省”的数据结构与算法为目标,对于算法效率评估主要分为两种:实际测试和理论估算。

实际测试

对于设计出的两个算法,直接在同一台计算机上运行,但是这种方式存在局限性。1 难以排除环境因素的干扰,不同硬件上不同算法运行的时间可能不一样,需要评估平均效率,不太现实。2 完整的测试太消耗资源,不同数据的输入也会对算法的效率产生影响,为了得到有说服力的结论,需要对不同的数据输入进行测试。

理论估算

由于实际测试具有较大的局限性,我们可以考虑仅通过一些计算来评估算法的效率,这种方法被简称为复杂度分析。

复杂度分析体现算法运行所需的时间(空间)资源与输入数据大小之间的关系。它描述了随着输入数据大小的增加,算法执行所需时间和空间的增长趋势。可以分为3个重点来理解:

        “时间和空间资源”分别对应「时间复杂度time complexity」和「空间复杂度space complexity」。

        “随着输入数据大小的增加”意味着复杂度反映了算法运行效率与输入数据体量之间的关系。

        “时间和空间的增长趋势”表示复杂度分析关注的不是运行时间或占用空间的具体值,而是时间或空间增长的“快慢”。


2.2 迭代与递归

迭代

迭代是一种重复执行某个任务的控制结构。在迭代中,程序会在满足一定的条件下重复执行某
段代码,直到这个条件不再满足。

for循环

for 循环是最常见的迭代形式之一,适合预先知道迭代次数时使用。

while循环

与for 循环类似,while 循环也是一种实现迭代的方法。在while 循环中,程序每轮都会先检查条件,如果条件为真则继续执行,否则就结束循环。

在while 循环中,由于初始化和更新条件变量的步骤是独立在循环结构之外的,因此它比for 循环的自由度更高。

for 循环的代码更加紧凑,while 循环更加灵活

嵌套循环

添加嵌套循环,每一次嵌套都是一次“升维”,将会使时间复杂度提高至“立方关系”、“四次方
关系”、以此类推。

递归

递归是一种算法策略,通过函数调用自身来解决问题。它主要包含两个阶段。
        1. 递:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。
        2. 归:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。
而从实现的角度看,递归代码主要包含三个要素。
        1. 终止条件:用于决定什么时候由“递”转“归”。
        2. 递归调用:对应“递”,函数调用自身,通常输入更小或更简化的参数。
        3. 返回结果:对应“归”,将当前递归层级的结果返回至上一层。

虽然从计算角度看,迭代与递归可以得到相同的结果,但它们代表了两种完全不同的思考和解决问题的范式。
        ‧ 迭代:“自下而上”地解决问题。从最基础的步骤开始,然后不断重复或累加这些步骤,直到任务完成。
        ‧ 递归:“自上而下”地解决问题。将原问题分解为更小的子问题,这些子问题和原问题具有相同的形式。
接下来将子问题继续分解为更小的子问题,直到基本情况时停止(基本情况的解是已知的)。

调用栈

递归函数每次调用自身时,系统都会为新开启的函数分配内存,以存储局部变量、调用地址和其他信息等。
这将导致两方面的结果。
        ‧ 函数的上下文数据都存储在称为“栈帧空间”的内存区域中,直至函数返回后才会被释放。因此,递归通常比迭代更加耗费内存空间。
        ‧ 递归调用函数会产生额外的开销。因此递归通常比循环的时间效率更低。

尾递归

如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空
间效率上与迭代相当。这种情况被称为「尾递归tail recursion」。
        ‧ 普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下文。
        ‧ 尾递归:递归调用是函数返回前的最后一个操作,这意味着函数返回到上一层级后,无需继续执行其他操作,因此系统无需保存上一层函数的上下文。

递归树

当处理与“分治”相关的算法问题时,递归往往比迭代的思路更加直观、代码更加易读。

本质上看,递归体现“将问题分解为更小子问题”的思维范式,这种分治策略是至关重要的。
        ‧ 从算法角度看,搜索、排序、回溯、分治、动态规划等许多重要算法策略都直接或间接地应用这种思维方式。
        ‧ 从数据结构角度看,递归天然适合处理链表、树和图的相关问题,因为它们非常适合用分治思想进行分析。


2.3 时间复杂度

运行时间可以直观且准确地反映算法的效率。如果我们想要准确预估一段代码的运行时间,应该完成以下操作:

1. 确定运行平台        2. 评估各种计算操作所需的运行时间        3. 统计代码中所有的计算操作

但实际上,统计算法的运行时间既不合理也不现实。首先,我们不希望将预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。

统计时间增长趋势

时间复杂度分析统计的不是算法运行时间,而是算法运行时间随着数据量变大时的增长趋势

“时间增长趋势”这个概念比较抽象,我们通过一个例子来加以理解。假设输入数据大小为𝑛 ,给定三个算法函数A、B 和C :

// 算法A 的时间复杂度:常数阶
void algorithm_A(int n) 
{
cout << 0 << endl;
}
// 算法B 的时间复杂度:线性阶
void algorithm_B(int n)
 {
for (int i = 0; i < n; i++)
    {cout << 0 << endl;}
}
// 算法C 的时间复杂度:常数阶
void algorithm_C(int n) 
{
for (int i = 0; i < 1000000; i++) 
    {cout << 0 << endl;}
}

算法A 只有1 个打印操作,算法运行时间不随着𝑛 增大而增长。我们称此算法的时间复杂度为“常数阶”。
算法B 中的打印操作需要循环𝑛 次,算法运行时间随着𝑛 增大呈线性增长。此算法的时间复杂度被称为“线性阶”。
算法C 中的打印操作需要循环1000000 次,虽然运行时间很长,但它与输入数据大小𝑛 无关。因此C的时间复杂度和A 相同,仍为“常数阶”

 相较于直接统计算法运行时间,时间复杂度分析有以下特点:

        时间复杂度能够有效评估算法效率

        时间复杂度的推算方法更简便

        时间复杂度也存在一定的局限性

函数渐近上界

给定一个输入大小为𝑛 的函数:

void algorithm(int n)
{
    int a = 1; // +1
    a = a + 1; // +1
    a = a * 2; // +1
    // 循环n 次
    for (int i = 0; i < n; i++)
        { // +1(每轮都执行i ++)
        cout << 0 << endl; // +1
        }
}

 设算法的操作数量是一个关于输入数据大小𝑛 的函数,记为𝑇 (𝑛) ,则以上函数的的操作数量为:𝑇 (𝑛) = 3 + 2𝑛。𝑇 (𝑛) 是一次函数,说明其运行时间的增长趋势是线性的,因此它的时间复杂度是线性阶。

我们将线性阶的时间复杂度记为O(𝑛) ,这个数学符号称为大O 记号big‑𝑂 notation,表示函数𝑇 (𝑛)的渐近上界。

推算方法

        第一步:统计操作数量

        针对代码,逐行从上到下计算即可。操作数量𝑇 (𝑛) 中的各种系数、常数项都可以被忽略。

                忽略𝑇 (𝑛) 中的常数项。

                省略所有系数。

                循环嵌套时使用乘法。

        第二步:判断渐近上界

        时间复杂度由多项式𝑇 (𝑛) 中最高阶的项来决定。

常见类型

设输入数据大小为𝑛 ,常见的时间复杂度类型(从低到高的顺序排列)

最差、最佳、平均时间复杂度

 假设输入一个长度为𝑛 的数组nums ,其中nums 由从1 至𝑛 的数字组成,每个数字只出现一次;但元素顺序是随机打乱的,任务目标是返回元素1 的索引。我们可以得出以下结论。

        当nums = [?, ?, ..., 1] ,即当末尾元素是1 时,需要完整遍历数组,达到最差时间复杂度𝑂(𝑛) 。
        当nums = [1, ?, ?, ...] ,即当首个元素为1 时,无论数组多长都不需要继续遍历,达到最佳时间复杂度Ω(1) 。

最差时间复杂度更为实用,因为它给出了一个效率安全值。平均时间复杂度可以体现算法在随机输入数据下的运行效率


2.4 空间复杂度

算法相关空间

算法在运行过程中使用的内存空间主要包括以下几种。

        ‧ 输入空间:用于存储算法的输入数据。
        ‧ 暂存空间:用于存储算法在运行过程中的变量、对象、函数上下文等数据。
        ‧ 输出空间:用于存储算法的输出数据。

一般情况下,空间复杂度的统计范围是“暂存空间”加上“输出空间”。

暂存空间可以进一步划分为三个部分。
        ‧ 暂存数据:用于保存算法运行过程中的各种常量、变量、对象等。
        ‧ 栈帧空间:用于保存调用函数的上下文数据。系统在每次调用函数时都会在栈顶部创建一个栈帧,函数返回后,栈帧空间会被释放。
        ‧ 指令空间:用于保存编译后的程序指令,在实际统计中通常忽略不计。

在分析一段程序的空间复杂度时,我们通常统计暂存数据、栈帧空间和输出数据三部分。

 推算方法

空间复杂度的推算方法与时间复杂度大致相同,只需将统计对象从“操作数量”转为“使用空间大小”。

我们通常只关注最差空间复杂度。这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。

观察以下代码,最差空间复杂度中的“最差”有两层含义。

        以最差输入数据为准

        以算法运行中的峰值内存为准

常见类型

设输入数据大小为𝑛,常见的空间复杂度类型(从低到高排列)。

 权衡时间与空间

理想情况下,我们希望算法的时间复杂度和空间复杂度都能达到最优。然而在实际情况中,同时优化时间复杂度和空间复杂度通常是非常困难的。降低时间复杂度通常需要以提升空间复杂度为代价,反之亦然。

三、数据结构 

3.1 数据结构分类

常见的数据结构包括数组、链表、栈、队列、哈希表、树、堆、图,它们可以从“逻辑结构”和“物理结构”两个维度进行分类。

逻辑结构:线性和非线性

逻辑结构揭示了数据元素之间的逻辑关系。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。

物理结构:连续与离散

在算法运行过程中,相关数据都存储在内存中。系统通过内存地址来访问目标位置的数据。内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用了。因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素。

物理结构反映了数据在计算机内存中的存储方式,可分为连续空间存储(数组)和离散空间存储(链表)。

所有数据结构都是基于数组、链表或二者的组合实现的。

3.2 基本数据类型

基本数据类型是CPU 可以直接进行运算的类型,在算法中直接被使用,主要包括以下几种类型。

‧ 整数类型byte、short、int、long 。
‧ 浮点数类型float、double ,用于表示小数。
‧ 字符类型char ,用于表示各种语言的字母、标点符号、甚至表情符号等。
‧ 布尔类型bool ,用于表示“是”与“否”判断。

基本数据类型以二进制的形式存储在计算机中。基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”。

四、数组和链表

 所有数据结构都是基于数组、链表或二者的组合实现的。对于数据的操作:创建销毁、增删改查、求表长、输出操作、判空操作

4.1 数组(静态数组)

「数组array」是一种线性数据结构,其将相同类型元素存储在连续的内存空间中。我们将元素在数组中的位置称为该元素的「索引index」

数组的基本操作

初始化(创建)

数组的两种初始化方式:无初始值(不推荐)、给定初始值。如果不知道数组防止什么元素,就设置为空。

int arr[5] = { 0 };
int nums[5] { 1, 3, 2, 5, 4 };
int* nums1 = new int[5] { 1, 3, 2, 5, 4 };
访问元素

数组元素被存储在连续的内存空间中,这意味着计算数组元素的内存地址非常容易。给定数组内存地址(即首元素内存地址)和某个元素的索引,从地址计算公式的角度看,索引的含义本质上是内存地址的偏移量。

插入元素(增)

如果想要在数组中间插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。但是由于数组的长度是固定的,因此插入一个元素必定会导致数组尾部元素“丢失”。

/* 在数组的索引index 处插入元素num */
void insert(int *nums, int size, int num, int index) {
// 把索引index 以及之后的所有元素向后移动一位
for (int i = size - 1; i > index; i--) {
nums[i] = nums[i - 1];
}
// 将num 赋给index 处元素
nums[index] = num;
}
删除元素(删)

若想要删除索引𝑖 处的元素,则需要把索引𝑖 之后的元素都向前移动一位。

/* 删除索引index 处元素*/
void remove(int *nums, int size, int index) {
// 把索引index 之后的所有元素向前移动一位
for (int i = index; i < size - 1; i++) {
nums[i] = nums[i + 1];
}
}
遍历数组(输出操作)
/* 遍历数组*/
void traverse(int *nums, int size) {
int count = 0;
// 通过索引遍历数组
for (int i = 0; i < size; i++) {
count++;
cout<<nums[count];
}
}
查找元素(查)

在数组中查找指定元素需要遍历数组,每轮判断元素值是否匹配,若匹配则输出对应索引。

/* 在数组中查找指定元素*/
int find(int *nums, int size, int target) {
for (int i = 0; i < size; i++) {
if (nums[i] == target)
return i;
}
return -1;
}
扩容数组

数组的长度在创建完成后是不可变的。如果想要对数组进行扩容,重新建立一个更大的数组,然后把原数组元素依次拷贝到新数组。

/* 扩展数组长度*/
int *extend(int *nums, int size, int enlarge) {
// 初始化一个扩展长度后的数组
int *res = new int[size + enlarge];
// 将原数组中的所有元素复制到新数组
for (int i = 0; i < size; i++) {
res[i] = nums[i];
}
// 释放内存
delete[] nums;
// 返回扩展后的新数组
return res;
}

数组优点与局限性

优点

        ‧ 空间效率高: 数组为数据分配了连续的内存块,无须额外的结构开销。
        ‧ 支持随机访问: 数组允许在𝑂(1) 时间内访问任何元素。
        ‧ 缓存局部性: 当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。

局限性

        ‧ 插入与删除效率低: 当数组中元素较多时,插入与删除操作需要移动大量的元素。
        ‧ 长度不可变: 数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。
        ‧ 空间浪费: 如果数组分配的大小超过了实际所需,那么多余的空间就被浪费了。

4.2 链表

「链表linked list」是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“指针”相连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。链表的设计使得各个节点可以被分散存储在内存各处,它们的内存地址是无须连续的。

链表的组成单位是「节点node」对象。每个节点都包含两项数据:节点的“值”和指向下一节
点的“指针”

‧ 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。
‧ 尾节点指向的是“空”,C++ 被记为nullptr 

链表节点ListNode 除了包含值,还需额外保存一个引用(指针)。因此在相同数据量下,链
表比数组占用更多的内存空间

结构体不能包含结构体自身,但是可以含有自身的结构体指针

/* 链表节点结构体*/
struct ListNode {
int val; // 节点值
ListNode *next; // 指向下一节点的指针
ListNode(int x) : val(x), next(nullptr) {} // 构造函数,初始化列表实现
};

链表常用操作

-- 链表只能通过动态分配内存实现,

初始化链表(创建)

建立链表分为两步,第一步是初始化各个节点对象(根据需求编写上述的构造函数),第二步是构建引用指向关系

初始化完成后,我们就可以从链表的头节点出发,通过引用指向next 依次访问所有节点。

/* 初始化链表1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
ListNode* n0 = new ListNode(1);
ListNode* n1 = new ListNode(3);
ListNode* n2 = new ListNode(2);
ListNode* n3 = new ListNode(5);
ListNode* n4 = new ListNode(4);
// 构建引用指向
n0->next = n1;
n1->next = n2;
n2->next = n3;
n3->next = n4;

通常将头节点当作链表的代称,比如以上代码中的链表可被记做链表n0 。 

插入节点(增)

假设我们想在相邻的两个节点n0 和n1 之间插入一个新节点P ,则只需要改变两个节点引用(指针)即可

/* 在链表的节点n0 之后插入节点P */
void insert(ListNode *n0, ListNode *P) {
ListNode *n1 = n0->next;    //先表明之前n0节点指向
P->next = n1;
n0->next = P;
}
删除节点(删)

在链表中删除节点也非常方便,只需改变一个节点的引用(指针)即可

/* 删除链表的节点n0 之后的首个节点*/
void remove(ListNode *n0) {
if (n0->next == nullptr)
return;
// n0 -> P -> n1
ListNode *P = n0->next;
ListNode *n1 = P->next;
n0->next = n1;
// 释放内存
delete P;
}
访问节点

在链表访问节点的效率较低。程序需要从头节点出发,逐个向后遍历,直至找到目标节点。

/* 访问链表中索引为index 的节点*/
ListNode *access(ListNode *head, int index) {
    for (int i = 0; i < index; i++) {
        if (head == nullptr)
            return nullptr;
        head = head->next;
    }
    return head;
}
查找节点

遍历链表,查找链表内值为target 的节点,输出节点在链表中的索引。

/* 在链表中查找值为target 的首个节点*/
int find(ListNode *head, int target) {
    int index = 0;
    while (head != nullptr) {
        if (head->val == target)
            return index;
        head = head->next;
        index++;
    }
    return -1;
}

数组VS 链表

常见链表类型

常见的链表类型包括三种。
‧ 单向链表:即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的引用两项数据。我们将首个节点称为头节点,将最后一个节点称为尾节点,尾节点指向空None 。
‧ 环形链表:如果我们令单向链表的尾节点指向头节点(即首尾相接),则得到一个环形链表。在环形链表中,任意节点都可以视作头节点。
‧ 双向链表:与单向链表相比,双向链表记录了两个方向的引用。双向链表的节点定义同时包含指向后继节点(下一个节点)和前驱节点(上一个节点)的引用(指针)。相较于单向链表,双向链表更具灵活性,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。

 结构体不能包含结构体自身,但是可以含有自身的结构体指针

/* 双向链表节点结构体*/
struct ListNode {
int val; // 节点值
ListNode *next; // 指向后继节点的指针
ListNode *prev; // 指向前驱节点的指针
ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // 构造函数
};

4.3 列表(动态数组)

数组长度不可变导致实用性降低。为解决此问题,出现了一种被称为「动态数组dynamic array」的数据结构,即长度可变的数组,也常被称为「列表list」。列表基于数组实现,继承了数组的优点,并且可以在程序运行过程中动态扩容。我们可以在列表中自由地添加元素,而无须担心超过容量限制。

列表常用操作

初始化列表

通常使用“无初始值”和“有初始值”这两种初始化方法。

/* 初始化列表*/
// 需注意,C++ 中vector 即是本文描述的list
// 无初始值
vector<int> list1;
// 有初始值
vector<int> list = { 1, 3, 2, 5, 4 };
访问元素

 列表本质上是数组

/* 访问元素*/
int num = list[1]; // 访问索引1 处的元素
/* 更新元素*/
list[1] = 0; // 将索引1 处的元素更新为0
插入与删除元素

 相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为𝑂(1) ,但插入和删除元素的效率仍与数组相同,时间复杂度为𝑂(𝑛) 。

/* 清空列表*/
list.clear();
/* 尾部添加元素*/
list.push_back(1);
list.push_back(3);
list.push_back(2);
list.push_back(5);
list.push_back(4);
/* 中间插入元素*/
list.insert(list.begin() + 3, 6); // 在索引3 处插入数字6
/* 删除元素*/
list.erase(list.begin() + 3); // 删除索引3 处的元素
遍历列表

 与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。

/* 通过索引遍历列表*/
int count = 0;
for (int i = 0; i < list.size(); i++) {
count++;
}
/* 直接遍历列表元素*/
count = 0;
for (int n : list) {
count++;
}
拼接列表

 给定一个新列表list1 ,我们可以将该列表拼接到原列表的尾部。

/* 拼接两个列表*/
vector<int> list1 = { 6, 8, 7, 10, 9 };
// 将列表list1 拼接到list 之后
list.insert(list.end(), list1.begin(), list1.end());
排序列表

 完成列表排序后,我们便可以使用在数组类算法题中经常考察的“二分查找”和“双指针”算法。

/* 排序列表*/
sort(list.begin(), list.end()); // 排序后,列表元素从小到大排列

列表实现

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据结构学习资料分享 内容概览: 本次分享包涵了大学计算机相关专业必学的“数据结构”课程的一系列学习资料。主要包括: 算法代码:我们提供了多种数据结构的实现代码,包括数组、链表、栈、队列、树、图等。这些代码不仅能帮助你理解数据结构的基本概念,而且能让你明白如何在实际情况中应用这些数据结构。 笔记:详细且系统的笔记,涵盖了数据结构的各个方面,从基础概念到复杂的数据结构如堆、B树等。这些笔记有助于你系统地复习和学习数据结构。 相关书籍推荐:为了更深入地理解数据结构,我们推荐了几本经典的教材和参考书籍。这些书籍将帮助你建立完整的数据结构知识体系。 适用人群: 这份学习资料适用于所有大学计算机相关专业的学生,无论你是初学者还是已经有一定的数据结构基础。同时,对于对数据结构感兴趣的非专业人士,这份资料也是一个很好的起点。 使用建议: 结合理论和实践:在学习的过程中,请结合算法代码和理论知识。尝试自己编写代码实现数据结构,并在遇到问题时参考提供的代码。 由浅入深:建议先从基础的数据结构开始学习,如数组和链表,然后再学习更复杂的数据结构如树和图。 多做练习:数据结构是实践性很强的学科。通过多做练习,你可以更好地理解数据结构的基本概念和原理,并提高编程能力。
数据结构是计算机存储、组织数据的方式,算法是解决问题的步骤和方法。在Shell编程中,常用的数据结构包括数组、关联数组和字符串,常用的算法包括循环、条件语句和函数等。 1. 数组 数组是一种线性数据结构,可以存储一组相同类型的数据。在Shell中,可以使用以下语法定义数组: ```shell array_name=(value1 value2 ... valuen) ``` 其中,array_name是数组名,value1、value2等是数组元素的值。可以使用以下语法访问数组元素: ```shell ${array_name[index]} ``` 其中,index是数组元素的下标,从0开始。 2. 关联数组 关联数组是一种键值对数据结构,可以根据键访问值。在Shell中,可以使用以下语法定义关联数组: ```shell declare -A assoc_array ``` 其中,assoc_array是关联数组名。可以使用以下语法访问关联数组元素: ```shell ${assoc_array[key]} ``` 其中,key是关联数组元素的键。 3. 字符串 字符串是一种字符序列数据结构,可以存储文本信息。在Shell中,可以使用以下语法定义字符串: ```shell string="hello world" ``` 其中,string是字符串变量名,hello world是字符串的值。可以使用以下语法访问字符串中的字符: ```shell ${string:index:length} ``` 其中,index是字符的下标,从0开始,length是要获取的字符数。 4. 循环 循环是一种重复执行某个操作的算法。在Shell中,可以使用以下语法实现循环: ```shell for var in list do command1 command2 ... done ``` 其中,var是循环变量,list是要循环的列表,command1、command2等是要执行的命令。 5. 条件语句 条件语句是一种根据条件执行不同操作的算法。在Shell中,可以使用以下语法实现条件语句: ```shell if condition then command1 command2 ... elif condition2 then command3 command4 ... else command5 command6 ... fi ``` 其中,condition、condition2等是条件表达式,command1、command2等是要执行的命令。 6. 函数 函数是一种封装了一组操作的算法。在Shell中,可以使用以下语法定义函数: ```shell function_name () { command1 command2 ... } ``` 其中,function_name是函数名,command1、command2等是函数体中要执行的命令。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值