#C++ Day42 Basic Data Structure Chapter3  review December 18 2025

要理解为什么SequentialList myList;不用加*,需要从变量的类型本质内存存储方式以及函数参数的设计意图三个层面来分析:

一、先明确两个核心概念:普通变量 vs 指针变量

C++ 中变量的声明决定了它的内存形态使用方式

1. SequentialList myList;:声明结构体的实际实例(普通变量)

  • 这行代码在栈内存中创建了一个SequentialList类型的完整实例,它本身占据实际的内存空间,包含三个成员:
    • eleType* elements(指针成员)
    • int capacity(整型成员)
    • int size(整型成员)
  • 你可以直接通过点运算符(.)访问它的成员(比如myList.size),也可以通过&myList获取它的内存地址(这是一个SequentialList*类型的指针)。

2. SequentialList* myList;:声明指向结构体的指针变量

  • 这行代码只在栈内存中创建了一个指针变量(大小通常为 4/8 字节,取决于系统),它的作用是存储一个内存地址,但本身并不是结构体实例
  • 此时myList是一个野指针(初始值随机),它没有指向任何有效的SequentialList实例。如果直接使用myList->capacity,会访问非法内存,导致程序崩溃。
  • 若要使用指针,必须先为它分配有效的实例,比如:cpp

    运行




    SequentialList* myList = new SequentialList(); // 堆上创建结构体实例,指针指向该实例




二、结合代码中函数的参数设计分析

代码中的函数(如initializeList、insertElement等)的参数都是SequentialList* list(结构体指针),这是为了实现对原结构体实例的修改(C++ 函数参数默认是值传递,传指针可以避免拷贝,且能修改原变量)。

但这并不意味着我们必须声明指针变量,因为:

  • 对于普通变量myList,我们可以通过&myList(取地址运算符)获取它的地址,这个地址的类型正好是SequentialList*,可以直接传给函数。
    比如:initializeList(&myList, 10); 就是把myList的地址传给initializeList的list参数,函数内部通过指针list操作的就是外部的myList实例。

如果我们声明的是指针变量SequentialList* myList;,反而需要额外的初始化步骤(否则是野指针),这会增加代码的复杂度,而这里完全没有必要。

三、两种写法的内存对比

写法内存位置是否是有效实例函数调用方式内存管理
SequentialList myList;是(完整实例)initializeList(&myList, 10);栈内存由系统自动释放
SequentialList* myList;栈(指针)否(野指针)需先myList = new SequentialList();,再initializeList(myList, 10);堆内存需手动delete myList;

四、总结:为什么这里不用加*

  • 需求是 “结构体实例” 而非 “指针”:我们需要的是一个能存储capacity、size和elements的实际结构体对象,而SequentialList myList;直接创建了这个对象,满足需求。
  • 传地址即可适配指针参数:函数需要SequentialList*参数,通过&myList取普通变量的地址,就能完美匹配参数类型,无需额外声明指针变量。
  • 避免野指针风险:如果声明SequentialList* myList;而不初始化,会导致野指针,引发程序崩溃;而普通变量是栈上的有效实例,无此风险。

拓展:如果非要用指针怎么办?

如果坚持用SequentialList* myList;,代码需要修改为:

cpp

运行

// 声明指针并在堆上创建实例

SequentialList* myList = new SequentialList();

// 调用函数(直接传指针,无需&)

initializeList(myList, 10);

// … 其他操作 …

// 不仅要释放elements,还要释放结构体本身

destroyList(myList);

delete myList; // 释放堆上的SequentialList实例

这种写法更复杂,且容易忘记delete myList导致内存泄漏,因此原代码的写法更简洁、安全。

当顺序表的容量已满时,需要进行…-知识星球

Chen 提问:

为什么这个地方是capacity*2而不是capacity++

当顺序表的容量已满时,需要进行扩容。扩容的方式是将容量扩大为原来的两倍,即 L->capacity * 2。这样做的原因是为了减少频繁扩容的次数,提高顺序表的性能。 因为扩容操作本身需要将所有元素重新进行一次拷贝,时间复杂度 O(n),如果每次扩容只是将容量增加1,那么当顺序表中的元素数量不断增加时,扩容的次数会非常频繁,从而导致性能下降。而将容量扩大为原来的两倍,可以有效地减少扩容的次数,确保算上扩容以后,均摊时间复杂度为 O(1),提高顺序表的效率。 为了均摊时间复杂度是O(1)呢? 某一次内存重分配完了以后, Capacity 达到了 n,那么表示这里有 n/2 个位置的元素是空的,剩下 n/2 的一半元素被拷贝了1次,剩下的一半的一半的元素拷贝了2次,… 那么所有元素被拷贝的次数就是: n/4 * 1 + n/8 * 2 + n/16 * 4 + … = n 所以当插入 n 个元素后,拷贝的总时间复杂度就是 O(n),每个元素的均摊时间复杂度就是 O(1)。  试着用最简单的语言解释

已完成思考

我们用生活化的例子 + 最直白的计算,把这个问题拆成两部分讲清楚:

一、先搞懂:为什么不选capacity++(每次只加 1)?

假设顺序表的扩容是 “满了就加 1”,把这个过程想象成搬仓库

  • 你有个小仓库,只能放 1 个箱子(容量 1),放满后,要换能放 2 个的仓库,得把1 个箱子搬过去(拷贝 1 个元素);
  • 再放 1 个,仓库又满了,换能放 3 个的仓库,得把2 个箱子搬过去(拷贝 2 个元素);
  • 再放 1 个,又满了,换能放 4 个的仓库,得把3 个箱子搬过去(拷贝 3 个元素);
    ……
    直到仓库能放n个箱子,你总共搬了1+2+3+…+(n-1)次箱子,这个总数约等于n²/2(比如 n=100,就要搬约 5000 次)。

简单说:每次只加 1,扩容太频繁,搬箱子(拷贝元素)的总次数会爆炸式增长,性能差到没法用。

二、再理解:为什么选capacity*2(每次扩 2 倍)?

还是用搬仓库的例子,这次满了就换2 倍大的仓库:

  • 初始仓库能放 1 个,放满后换能放 2 个的,搬1 个箱子;
  • 放满 2 个后,换能放 4 个的,搬2 个箱子;
  • 放满 4 个后,换能放 8 个的,搬4 个箱子;
  • 放满 8 个后,换能放 16 个的,搬8 个箱子;
    ……
    直到仓库能放n个箱子,你总共搬了1+2+4+8+…次,这个总数永远小于 n(比如 n=16,只搬了 1+2+4+8=15 次;n=32,只搬了 31 次)。

核心差异:2 倍扩容让 “搬仓库” 的次数极少,总搬箱次数和最终的箱子数差不多

三、最后说:为什么均摊时间复杂度是 O (1)?

“均摊时间复杂度” 就是把所有搬箱子的耗时,平摊到每一次 “放箱子” 的操作上,看平均每次放箱子要花多少时间。

举个具体例子:插入 8 个元素(箱子),用 2 倍扩容:

  • 插入第 1 个:仓库满了,扩容到 2,搬 1 个箱子(总耗时:1 次插入 + 1 次搬);
  • 插入第 2 个:仓库满了,扩容到 4,搬 2 个箱子(总耗时:1 次插入 + 2 次搬);
  • 插入第 3、4 个:不用扩容(总耗时:2 次插入);
  • 插入第 4 个后满了,扩容到 8,搬 4 个箱子(总耗时:1 次插入 + 4 次搬);
  • 插入第 5-8 个:不用扩容(总耗时:4 次插入)。

总耗时统计

  • 插入操作:8 次(每个元素 1 次);
  • 搬箱子(拷贝):1+2+4=7 次;
  • 总计:8+7=15 次操作。

平均到每个元素:15÷8≈1.875 次操作 —— 这个数是固定的常数(不管插入多少元素,平均下来每次都接近 1 次操作),这就是均摊时间复杂度 O (1)

一句话总结

每次扩容 2 倍,虽然偶尔有一次插入要 “搬很多箱子”,但把这个耗时平摊到很多次不用搬的插入上,平均下来每次插入的耗时几乎不变;而每次只加 1,每插一次都要 “搬箱子”,越往后越慢。

剩下 n/2 的一半元素被拷贝了1次,剩下的一半的一半的元素拷贝了2次、

已完成思考

我们用具体的数值例子(比如最终容量 n=8) + 直白的分组,把这句话拆解得明明白白,保证一看就懂:

先明确前提(原文的场景)

某次扩容后容量达到 n(比如 n=8),因为是翻倍扩容,这次扩容是从n/2=4扩容到n=8的。

所以扩容后:

  • 总容量n=8,其中元素数量是 n/2=4(都是从旧数组拷贝过来的);
  • 剩下的n/2=4个位置是空的(这就是原文里 “这里有 n/2 个位置的元素是空的”)。

我们要统计的是:这 4 个元素(n/2)从被插入到现在,一共被拷贝了多少次

再拆解 “一半元素被拷贝 1 次,一半的一半拷贝 2 次……”

以n=8(容量)、n/2=4(元素数)为例:

把这 4 个元素按 **“加入顺序” 分组 **,看每组被拷贝的次数:

分组(元素数量)对应原文的描述被拷贝的次数具体解释
2 个元素(4 的一半)n/2 的一半(n/4=2)1 次这 2 个元素是在容量 4 时加入的,只经历了 “4→8” 这 1 次扩容,所以只被拷贝 1 次。
1 个元素(4 的一半的一半)n/2 的一半的一半(n/8=1)2 次这个元素是在容量 2 时加入的,经历了 “2→4” 和 “4→8”2 次扩容,所以被拷贝 2 次。
1 个元素(继续拆分)n/2 的一半的一半的一半3 次这个元素是在容量 1 时加入的,经历了 “1→2”“2→4”“4→8”3 次扩容,所以被拷贝 3 次。

如果把容量放大到n=16(更易观察规律),规律会更明显:

  • 扩容后容量 n=16,元素数 n/2=8;
  • 4 个元素(8 的一半):在容量 8 时加入,只经历 “8→16”,被拷贝 1 次;
  • 2 个元素(8 的一半的一半):在容量 4 时加入,经历 “4→8”“8→16”,被拷贝 2 次;
  • 1 个元素(8 的一半的一半的一半):在容量 2 时加入,经历 3 次扩容,被拷贝 3 次;
  • 1 个元素:在容量 1 时加入,经历 4 次扩容,被拷贝 4 次。

一句话翻译这句话的核心

“越早期加入的元素,被拷贝的次数越多;每往回追溯一次扩容,元素数量就减半,拷贝次数就加 1”

最后补一句:为什么总拷贝次数加起来约等于 n?

还是以 n=8 为例,总拷贝次数是:

2个×1次 + 1个×2次 + 1个×3次 = 2+2+3=7 ≈8(n)

以 n=16 为例,总拷贝次数是:

4个×1次 + 2个×2次 + 1个×3次 + 1个×4次 =4+4+3+4=15≈16(n)

这就是原文里 “所有元素被拷贝的次数加起来等于 n” 的原因,最终平摊到每个元素上,拷贝的平均次数就是常数级(O (1))。

#include <iostream>

using namespace std;

#define eleType int

//1.define

struct SequentialList {

eleType* elements;

int capacity;

int size;

};

//2.initialize

void initializeList(SequentialList* list, int capacity) {

list->elements = new eleType[capacity];

list->capacity = capacity;

list->size = 0;

}

//3.isEmpty and size

bool isEmpty(SequentialList* list) {

return list->size == 0;

}

int sizeOfList(SequentialList* list) {

return list->size;

}

//4.destroy

void destroyList(SequentialList* list) {

delete[] list->elements;

}

//5.insert

void insertElement(SequentialList* list, int index, eleType element) {

if (index<0 || index>list->size) {

throw std::invalid_argument(“invalid index”);

}

if (list->size == list->capacity) {

int Newcapacity = list->capacity * 2;

eleType* Newlist = new eleType[Newcapacity];

for (int i = 0; i < list->size; i++) {

Newlist[i] = list->elements[i];

}

delete[] list->elements;

list->elements = Newlist;

list->capacity = Newcapacity;

}

list->size++;//size要放上面

for (int i = list->size-1; i > index; i–) {//这里是list->size-1

list->elements[i] = list->elements[i – 1]; 

}

list->elements[index] = element;

}

//6.delete

void deleteElement(SequentialList* list, int index) {

if (index < 0 || index >= list->size) {

throw std::invalid_argument(“invalid index”);

}

for (int i = index; i < list->size-1; i++) {

list->elements[i] = list->elements[i + 1];

}

list->size–;

}

//7.find index

int findElementIndex(SequentialList* list, eleType element) {

for (int i = 0; i < list->size; i++) {

if (list->elements[i] == element) {

return i;

}

}

return -1;//represent not found

}

//8.getElement with index

eleType getElement(SequentialList* list, int index) {

if (index < 0 || index >= list->size) {

throw std::invalid_argument(“invalid index”);

}

return list->elements[index];

}

//9.update

void updateElement(SequentialList* list, int index, eleType element) {

if (index < 0 || index >= list->size) {

throw std::invalid_argument(“invalid index”);

}

list->elements[index] = element;

}

int main() {

SequentialList Mylist;

int list_capacity = 10;

initializeList(&Mylist, list_capacity);

cout << “IsEmpty:” << isEmpty(&Mylist) << endl;

cout << “Size:” << sizeOfList(&Mylist) << endl;

for (int i = 0; i < list_capacity; i++) {

insertElement(&Mylist, i, i * 10);

}

cout << “IsEmpty:” << isEmpty(&Mylist) << endl;

cout << “Size:” << sizeOfList(&Mylist)<<endl;

for (int i = 0; i < sizeOfList(&Mylist); i++) {

cout << getElement(&Mylist, i) << ” “;

}cout << endl;

deleteElement(&Mylist, 9);

cout << “IsEmpty:” << isEmpty(&Mylist) << endl;

cout << “Size:” << sizeOfList(&Mylist) << endl;

for (int i = 0; i < sizeOfList(&Mylist); i++) {

cout << getElement(&Mylist, i) << ” “;

}cout << endl;

insertElement(&Mylist, sizeOfList(&Mylist), 55);

for (int i = 0; i < sizeOfList(&Mylist); i++) {

cout << getElement(&Mylist, i) << ” “;

}cout << endl;

int idx = findElementIndex(&Mylist, 55);

updateElement(&Mylist, idx, 520);

for (int i = 0; i < sizeOfList(&Mylist); i++) {

cout << getElement(&Mylist, i) << ” “;

}cout << endl;

cout << “IsEmpty:” << isEmpty(&Mylist) << endl;

cout << “Size:” << sizeOfList(&Mylist) << endl;

destroyList(&Mylist);

return 0;

}