复杂度分析,数据结构的数组与链表

复杂度分析,数据结构的数组与链表

参考书籍:Hello算法

复杂度分析

复杂度分析是用来判断一个算法效率的手段,执行时所需的时间和空间资源则是正好对应时间和空间复杂度。

考虑到执行设备的硬件配置以及执行的编程语言,系统环境等都会对复杂度分析结果造成影响,所以只通过一些计算来评估代码的效率。

时间复杂度

是用来统计一个算法随着数据量的增大,其运行时间的增长趋势。计算时间复杂度时常数项和系数都都可以忽略,并且时间复杂度是由最高阶决定的。由于算法的效率不是固定的,一般情况下,只考虑最差时间复杂度,考虑算法最差需要多少时间。一个数组内1-n的整数,但是顺序被打乱,想从中找到1,当正好在1索引时则时间复杂度为最佳时间复杂度,当在数组的末尾时,则要达到最差时间复杂度为数组的大小。

空间复杂度

用于衡量算法占用内存空间随着数据量变大时的增长趋势。这个概念与时间复杂度非常类似,只需将“运行时间”替换为“占用内存空间”。我们通常只关注最差空间复杂度,即统计算法在最差输入数据和最差运行时刻下的空间复杂度。

数据结构

先区分数据类型和数据结构,我们平时常见的int、char、double以及布尔值等都是数据类型,而数据结构是指数组、链表、栈、队列、哈希表、树、堆、图一系列的数据类型的组织结构。数据结构又分为逻辑结构和物理结构。

逻辑结构指的是数据元素之间的逻辑关系,”线性和非线性“。

  • 线性数据结构:数组、链表、栈、队列、哈希表,元素之间是一对一的顺序关系。
  • 非线性数据结构:树、堆、图、哈希表。

非线性数据结构可以进一步划分为树形结构和网状结构。

  • 树形结构:树、堆、哈希表,元素之间是一对多的关系。
  • 网状结构:图,元素之间是多对多的关系。

物理结构反应的是数据在计算机内存中的存储,”连续和分散“。

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

  • 基于数组可实现:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 ≥3 的数组)等。
  • 基于链表可实现:栈、队列、哈希表、树、堆、图等。

链表在初始化后,仍可以在程序运行过程中对其长度进行调整,因此也称“动态数据结构”。数组在初始化后长度不可变,因此也称“静态数据结构”。值得注意的是,数组可通过重新分配内存实现长度变化,从而具备一定的“动态性”。

数组与链表

数组

数组是将数据线性连续存储在内存中的一种数据结构,特点是连续存储的。每个数据元素在数组中的位置是这个元素的索引,索引从0开始计算。其优点在于计算数组元素的内存地址方便。索引本质上是内存地址的偏移量,可以用过上篇博客看得出来。

由于数组长度在初始化后不可变,当初始化时,未指定初始值时,一般的初始值为0,因为数组元素位置连续,元素之间是没有位置可供存放数据的,如果想在数组中间插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。并且数组长度固定,插入时会导致数组尾部元素丢失。想删除某索引处的元素,则需要把该索引之后的元素都向前移动一位,删除元素完成后,原先末尾的元素变得“无意义”了,所以我们无须特意去修改它。

链表

链表也是线性的结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。链表的设计使得各个节点可以分散存储在内存各处,它们的内存地址无须连续。链表的组成单位是节点对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”。链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。尾节点指向的是“空”,在 C、C++等支持指针的语言中,上述“引用”应被替换为“指针”。

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

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

链表的插入和删除节点比较容易,进行指针指向的节点修改即可。但在链表中访问节点的效率较低。

常见的链表类型包括三种。

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

列表

列表是抽象的数据结构,可以由数组和链表来实现,它表示元素的有序集合,支持元素访问、修改、添加、删除和遍历等操作,无须使用者考虑容量限制的问题。但是数组由于长度时初始化时定义好的,所以其实现的列表的长度是不可变的,数组只能看成一个长度限制的列表,使用动态数组来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。

实例代码:

#include <iostream>
#include <cstdlib> // 包含rand()和srand()函数的头文件
#include <ctime> // 包含time()函数的头文件
#include <vector>

using namespace std;

class MyList {
  private:
    int *arr;             // 数组(存储列表元素)
    int arrCapacity = 10; // 列表容量
    int arrSize = 0;      // 列表长度(当前元素数量)
    int extendRatio = 2;   // 每次列表扩容的倍数

  public:
    /* 构造方法 */
    MyList() {
        arr = new int[arrCapacity];
    }

    /* 析构方法 */
    ~MyList() {
        delete[] arr;
    }

    /* 获取列表长度(当前元素数量)*/
    int size() {
        return arrSize;
    }

    /* 获取列表容量 */
    int capacity() {
        return arrCapacity;
    }

    /* 访问元素 */
    int get(int index) {
        // 索引如果越界,则抛出异常,下同
        if (index < 0 || index >= size())
            throw out_of_range("索引越界");
        return arr[index];
    }

    /* 更新元素 */
    void set(int index, int num) {
        if (index < 0 || index >= size())
            throw out_of_range("索引越界");
        arr[index] = num;
    }

    /* 在尾部添加元素 */
    void add(int num) {
        // 元素数量超出容量时,触发扩容机制
        if (size() == capacity())
            extendCapacity();
        arr[size()] = num;
        // 更新元素数量
        arrSize++;
    }

    /* 在中间插入元素 */
    void insert(int index, int num) {
        if (index < 0 || index >= size())
            throw out_of_range("索引越界");
        // 元素数量超出容量时,触发扩容机制
        if (size() == capacity())
            extendCapacity();
        // 将索引 index 以及之后的元素都向后移动一位
        for (int j = size() - 1; j >= index; j--) {
            arr[j + 1] = arr[j];
        }
        arr[index] = num;
        // 更新元素数量
        arrSize++;
    }

    /* 删除元素 */
    int remove(int index) {
        if (index < 0 || index >= size())
            throw out_of_range("索引越界");
        int num = arr[index];
        // 将索引 index 之后的元素都向前移动一位
        for (int j = index; j < size() - 1; j++) {
            arr[j] = arr[j + 1];
        }
        // 更新元素数量
        arrSize--;
        // 返回被删除的元素
        return num;
    }

    /* 列表扩容 */
    void extendCapacity() {
        // 新建一个长度为原数组 extendRatio 倍的新数组
        int newCapacity = capacity() * extendRatio;
        int *tmp = arr;
        arr = new int[newCapacity];
        // 将原数组中的所有元素复制到新数组
        for (int i = 0; i < size(); i++) {
            arr[i] = tmp[i];
        }
        // 释放内存
        delete[] tmp;
        arrCapacity = newCapacity;
    }

    /* 将列表转换为 Vector 用于打印 */
    vector<int> toVector() {
        // 仅转换有效长度范围内的列表元素
        vector<int> vec(size());
        for (int i = 0; i < size(); i++) {
            vec[i] = arr[i];
        }
        return vec;
    }
};


int main() {
    int capacity=0;
    int size=0;
    MyList a;
    
    capacity=a.capacity();
    size=a.size();
    cout<<"列表容量:"<<capacity<<endl;
    cout<<"列表大小:"<<size<<endl;
    
    for(int i=0 ; i<=20 ; i++){
        a.add(i);
    }
    vector<int> vec = a.toVector(); // 获取 MyList 中的元素
    for (int num : vec) { // 遍历并打印每个元素
        cout << num << " ";
    }
    cout << endl;
    capacity=a.capacity();
    size=a.size();
    cout<<"列表容量:"<<capacity<<endl;
    cout<<"列表大小:"<<size<<endl;
    
    for(int i=21 ; i<=50 ; i++){
        a.add(i);
    }
    vector<int> vec1 = a.toVector();
    for (int num : vec1) { // 遍历并打印每个元素
        cout << num << " ";
    }
    cout << endl;
    capacity=a.capacity();
    size=a.size();
    cout<<"列表容量:"<<capacity<<endl;
    cout<<"列表大小:"<<size<<endl;
   
    return 0;
}

运行结果:

列表容量:10
列表大小:0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 
列表容量:40
列表大小:21
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
列表容量:80
列表大小:51

出现中文乱码问题,解决方案:

问题:vscode 打印中文时终端输出乱码-CSDN博客

posted @ 2024-10-29 19:00  无情马里奥  阅读(101)  评论(0)    收藏  举报