7/25复习-数组、链表、栈、队列、堆

  

目录

1.数组

1.1简介

1.2优点

1.3缺点 

1.3.1常量指针 

1.3.1.1简介

1.3.1.2定义

1.3.1.3赋值

1.3.1.4解引用

1.3.1.5作为函数参数

1.3.1.6和指向常量的指针相比

1.3.2指向常量的指针

1.3.2.1简介

1.3.2.2定义

1.3.2.3赋值

1.3.2.4解引用

1.3.2.5作为函数参数

1.4底层实现

2.链表 

2.1简介

2.2优点

2.3缺点 

2.3.1缓存命中率

2.4底层实现 

3.栈 

3.1简介

3.2优点

3.3缺点 

3.4底层实现 

3.4.1栈的容量

4.队列

4.1简介

4.2优点

4.3缺点 

4.4底层实现 

5.堆 

5.1简介

5.2优点

5.3缺点 

5.4底层实现 

5.4.1内存碎片

5.4.1.1简介

5.4.1.2解决方法


        前几天将STL中的内容进行了讲解,今天我们来讲解一下C++中基础数据结构,包括数组、链表、栈、队列、堆、树、图、哈希。今天我们主要讲其中的数组、链表、栈、队列、堆。觉得我讲的不错可以点个赞,当然评论支持一下就更好啦,下面我们开始讲解。

1.数组

1.1简介

        一组相同类型的元素按照一定顺序排列而成的数据结构,可以使用下标访问数组中的元素。

1.2优点

  1. 效率高:数组在内存中是连续的一段存储空间,可以通过下标直接访问数组元素,因此速度快。
  2. 可以用于多维数组:数组可以定义为多维的,可以方便地表示和处理多维数据。

1.3缺点 

  1. 内存固定:数组定义时需要指定大小,且大小是固定的,这就意味着在使用数组时,必须保证数组大小足够,否则可能导致内存溢出或者浪费。
  2. 不支持动态增加:数组的大小是固定的,无法动态增加,如果需要动态增加,需要重新定义一个新数组,并将原数组的元素复制到新数组中。
  3. 数组名不可修改:数组名是常量指针,不能修改,所以不能通过修改数组名来改变数组的大小或位置。
  4. 不支持复制和赋值:数组不能直接复制和赋值,需要使用循环语句逐一复制和赋值,这会降低效率。

        其中第三点中我们谈到了一个新的知识,接受常量指针,下面我们对常量指针进行一个讲解。

1.3.1常量指针 

1.3.1.1简介

        常量指针是指指向常量对象的指针,也就是指针本身不能修改,指向的对象也不能通过指针来修改。

1.3.1.2定义

        常量指针可以通过在指针类型前加const关键字来定义,例如:const int *p;表示p是一个指向常量整型的指针。

1.3.1.3赋值

        常量指针可以被赋值,但是一旦指向了一个常量对象,就不能通过该指针来修改对象的值。

1.3.1.4解引用

        常量指针可以通过解引用操作符*来访问指向的对象的值,但是不能通过解引用操作符来修改对象的值。

1.3.1.5作为函数参数

        常量指针可以作为函数的参数进行传递,从而实现对常量对象的访问。

1.3.1.6和指向常量的指针相比

        常量指针和指向常量的指针都指向常量对象,但是它们的区别在于前者指针本身是常量,后者指向的对象是常量。

        既然说到了这里,下面就讲一下指向常量的指针。

1.3.2指向常量的指针

1.3.2.1简介

        指向常量的指针是指指向常量对象的指针,也就是指针可以被修改,但是指向的对象不能通过该指针来修改。

1.3.2.2定义

        指向常量的指针可以通过在指针类型前加const关键字来定义,例如:int const *p;表示p是一个指向常量整型的指针。

1.3.2.3赋值

        指向常量的指针可以被赋值,但是一旦指向了一个常量对象,就不能通过该指针来修改对象的值。

1.3.2.4解引用

        指向常量的指针可以通过解引用操作符*来访问指向的对象的值,但是不能通过解引用操作符来修改对象的值。

1.3.2.5作为函数参数

        指向常量的指针可以作为函数的参数进行传递,从而实现对常量对象的访问。

1.4底层实现

        底层实现通常是使用连续的内存空间来存储元素。具体来说,数组中的每个元素都被存储在相邻的内存地址中,可以通过下标来访问和修改数组中的元素。
        在内存中,数组通常被存储在堆栈或堆内存中。如果数组是在函数中定义的,它通常会被存储在堆栈中。当函数调用结束时,数组将被自动销毁。如果数组是使用new操作符动态分配的,它通常会被存储在堆内存中。当不再需要数组时,需要使用delete操作符来手动释放数组所占用的内存空间。
        在访问数组元素时,C++会将数组的首地址加上偏移量,得到需要访问的元素的地址。这个过程通常是由编译器自动完成的,因此访问数组元素的时间复杂度是O(1)。
需要注意的是,当数组元素的数量很大时,数组可能会占用过多的内存空间,因此需要考虑内存的限制。此外,当数组需要进行插入、删除等操作时,由于数组元素的存储是连续的,这些操作可能会导致数组的重新分配和元素的移动,因此可能会降低程序的性能。

2.链表 

2.1简介

        一组通过指针连接在一起的节点组成的数据结构,可以使用指针访问链表中的元素。

2.2优点

  1. 内存动态分配:链表节点可以在运行时动态分配内存,因此链表可以根据需要动态地增长或缩小。
  2. 插入和删除操作高效:链表的插入和删除操作非常高效,因为只需要改变指针的指向即可完成操作,而不需要像数组一样移动元素。
  3. 灵活性:链表可以轻松地实现各种数据结构,如栈、队列和哈希表等。

2.3缺点 

  1. 随机访问低效:链表不支持随机访问,只能从头开始遍历到指定位置,因此访问元素的时间复杂度为O(n)。
  2. 空间开销较大:链表需要额外的指针来维护节点之间的关系,因此它需要更多的内存空间来存储相同数量的元素。
  3. 缓存不友好:由于链表节点在内存中不是连续存储的,所以访问链表节点时缓存命中率较低,这会影响程序的性能。

         在上面的第三点当中提到了缓存命中率,我下面给大家讲解一下这个东西。

2.3.1缓存命中率

        缓存命中率是指CPU在访问内存时,从高速缓存中读取数据的比例。高速缓存是CPU与内存之间的一个缓冲区域,它可以存储最近访问的数据。由于高速缓存的读取速度比内存快得多,因此如果CPU能够从高速缓存中读取数据,程序的性能会得到很大的提升。
        然而,由于链表节点在内存中不是连续存储的,每次访问链表节点时,CPU需要从内存中读取一个节点的数据,并且还需要读取下一个节点的地址,这会导致高速缓存中的数据被频繁替换,缓存命中率会降低。相比之下,数组等连续存储的数据结构可以更好地利用高速缓存,因为它们的数据在内存中是连续存储的,CPU可以预取数据,提高数据读取的效率。
        因此,当需要频繁访问数据时,链表的性能可能会受到缓存命中率的影响,尤其是当链表非常长时。在这种情况下,可以考虑使用数组等连续存储的数据结构来提高程序的性能。

2.4底层实现 

        底层实现通常是通过指针来实现的。具体来说,链表由若干个节点组成,每个节点都包含一个数据元素和一个指向下一个节点的指针。通过这种方式,链表中的节点可以在内存中分散存储,不需要像数组一样占用连续的内存空间。
        在C++中,链表的节点通常是通过new操作符动态分配的,可以使用delete操作符手动释放节点所占用的内存空间。链表中的头节点通常是一个空节点,它的作用是指向链表中第一个数据元素的节点。
        在访问链表节点时,C++需要通过指针来获取下一个节点的地址,因此访问链表元素的时间复杂度是O(n)。在插入、删除等操作时,链表可以通过修改指针来完成,因此这些操作的时间复杂度通常是O(1)。
        需要注意的是,链表的底层实现需要手动管理内存,因此需要注意内存泄漏和空指针的问题。此外,由于链表中的节点是分散存储的,它可能会增加缓存不命中的概率,从而降低程序的性能。

3.栈 

3.1简介

        后进先出(LIFO)的数据结构,只允许在栈顶进行插入和删除操作,可以使用push、pop、top等操作。

3.2优点

  1. 快速插入和删除:栈采用后进先出(LIFO)的原则,所以插入和删除操作非常高效。
  2. 不需要动态内存分配:栈的大小在编译时就已经确定,因此不需要动态分配内存,这可以避免内存泄漏和内存碎片问题。
  3. 代码简洁:使用栈可以使代码更加简洁易懂,因为栈的操作非常直观。

3.3缺点 

  1. 大小固定:栈的大小在编译时就已经确定,因此无法动态调整大小。如果需要存储的元素数量超出了栈的大小,就会出现栈溢出的问题。
  2. 存储元素类型受限:栈只能存储相同类型的元素,而且通常只能存储较小的数据类型,如整数、字符等。
  3. 访问元素不方便:由于栈是后进先出的数据结构,因此访问栈中的元素不如数组等数据结构方便,需要先弹出栈顶元素,才能访问下一个元素。

3.4底层实现 

        底层实现通常是使用堆栈内存来存储数据。堆栈内存是一种后进先出(LIFO)的内存分配方式,它的分配和释放由编译器自动完成。
        在C++中,栈可以使用静态数组或动态数组来实现。静态数组是在编译时分配的,它的大小是固定的,不能动态调整。动态数组是使用new操作符在堆内存中动态分配的,它的大小可以根据需要动态调整。
        在访问栈元素时,C++使用指针来跟踪栈顶元素的位置。栈顶元素通常是最后一个被添加到栈中的元素,因此访问栈元素的时间复杂度是O(1)。在入栈和出栈操作时,栈可以通过移动指针来实现,因此这些操作的时间复杂度通常是O(1)。
        需要注意的是,栈内存通常是有限的,如果栈中存储的数据量过大,可能会导致栈溢出的问题。此外,栈内存中的数据只在当前函数的作用域中有效,当函数返回时,栈内存中的数据将被自动销毁。

3.4.1栈的容量

        栈内存的大小是由操作系统和编译器决定的,它通常是有限的。在不同的操作系统和编译器下,栈内存的大小可能会有所不同。
        在Windows操作系统下,默认的栈大小是1MB,在Linux和Unix操作系统下,栈大小通常是8MB或更大。需要注意的是,栈的大小限制也取决于系统的物理内存和虚拟内存的大小。如果栈内存超出了系统的物理内存或虚拟内存的大小限制,可能会导致栈溢出或程序崩溃的问题。

4.队列

4.1简介

        先进先出(FIFO)的数据结构,只允许在队尾插入元素,在队头删除元素,可以使用push、pop、front、back等操作。

4.2优点

  1. 快速插入和删除:队列采用先进先出(FIFO)的原则,所以插入和删除操作非常高效。
  2. 不需要动态内存分配:队列的大小在编译时就已经确定,因此不需要动态分配内存,这可以避免内存泄漏和内存碎片问题。
  3. 代码简洁:使用队列可以使代码更加简洁易懂,因为队列的操作非常直观。

4.3缺点 

  1. 大小固定:队列的大小在编译时就已经确定,因此无法动态调整大小。如果需要存储的元素数量超出了队列的大小,就会出现队列溢出的问题。
  2. 存储元素类型受限:队列只能存储相同类型的元素,而且通常只能存储较小的数据类型,如整数、字符等。
  3. 访问元素不方便:由于队列是先进先出的数据结构,因此访问队列中的元素不如数组等数据结构方便,需要先出队头元素,才能访问下一个元素。

4.4底层实现 

        底层实现通常是使用数组或链表来存储元素。具体来说,队列通常由一个数组或链表来存储元素,同时还包括一个头指针和一个尾指针。头指针指向队列中的第一个元素,尾指针指向队列中最后一个元素的下一个位置。
        在C++中,队列的元素可以使用静态数组、动态数组或链表来实现。静态数组是在编译时分配的,它的大小是固定的,不能动态调整。动态数组是使用new操作符在堆内存中动态分配的,它的大小可以根据需要动态调整。链表是一种动态数据结构,它可以动态添加和删除节点。
        在访问队列元素时,C++使用头指针和尾指针来跟踪队列中元素的位置。队列的插入、删除操作通常是在队列的尾部进行的,因此这些操作的时间复杂度是O(1)。访问队列中的元素通常是在队列的头部进行的,因此这些操作的时间复杂度也是O(1)。
        需要注意的是,在使用数组实现队列时,当队列中的元素数量过多时,可能会导致数组空间不足的问题。

5.堆 

5.1简介

        一种特殊的树形数据结构,满足堆序性质,可以用来实现优先队列和排序等算法。

5.2优点

  1. 动态内存管理:堆可以动态地分配和释放内存,这使得堆可以根据需要动态增长或缩小。
  2. 存储元素类型不受限:堆可以存储任意类型和大小的数据,包括自定义类型和结构体等。
  3. 访问元素方便:堆可以通过下标或指针来访问元素,非常方便。

5.3缺点 

  1. 内存管理复杂:由于堆可以动态分配和释放内存,因此它需要进行复杂的内存管理,包括内存分配、内存释放和内存碎片整理等。
  2. 内存泄漏和越界访问:堆的动态内存管理需要开发者自行管理,如果管理不当,容易出现内存泄漏和越界访问等问题。
  3. 访问元素速度较慢:堆的访问速度比栈和数组等数据结构要慢,因为它需要通过指针来访问元素,而指针的访问速度比直接访问数组元素要慢。

5.4底层实现 

        底层实现通常是使用动态内存分配来实现。动态内存分配是指在运行时根据需要动态分配内存空间,它的分配和释放由程序员手动控。
        在C++中,使用new操作符可以在堆内存中动态分配内存空间,使用delete操作符可以手动释放内存空间。堆内存中的数据可以被多个函数共享,在函数调用结束后,数据仍然可以保持存在,直到手动释放内存空间。
        在访问堆内存中的数据时,C++使用指针来访问堆内存中的数据。在使用堆内存时,需要注意内存泄漏和空指针的问题。内存泄漏是指在动态分配内存后,没有手动释放内存空间,导致内存无法被回收。空指针是指指向NULL地址的指针,它无法访问有效的内存空间,可能会导致程序崩溃或数据损坏的问题。
        需要注意的是,在使用堆内存时,需要避免过度分配内存空间,以免浪费内存资源。同时,也需要避免内存碎片的问题,这可能会导致无法分配大块内存空间的问题。

5.4.1内存碎片

5.4.1.1简介

        内存碎片是指堆内存中存在一些零散的未使用内存块,这些内存块虽然总和大小可能足够大,但是由于它们分散在堆内存的各个角落,无法满足大块连续内存的分配需求。这就可能导致无法分配大块内存空间的问题。
        例如,假设程序先分配了若干个小块内存空间,然后逐渐释放这些内存空间。如果这些小块内存空间分散在堆内存的不同位置,就可能导致堆内存中出现一些较小的未使用内存块,这些未使用内存块可能无法满足大块内存的分配需求,导致分配失败。

5.4.1.2解决方法
  1. 使用内存池等技术来优化内存分配效率。内存池是指在程序启动时预先分配一定数量的内存空间,然后在程序运行期间重复使用这些内存空间。这样可以避免频繁地进行内存分配和释放,从而减少内存碎片的产生,提高内存分配效率。
  2. 使用内存合并等技术来解决内存碎片问题。内存合并是指在释放内存时,尝试将相邻的未使用内存块合并成一个大的未使用内存块,从而减少内存碎片的产生。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

puzell

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值