小肥柴慢慢手写数据结构(C篇)(2.1.1 动态数组(ArrayList))

小肥柴慢慢手写数据结构(C篇)(2.1.1 动态数组(ArrayList))

  • 目录
    • [2.1.1.1 问题的出现](#2.1.1.1 问题的出现)
    • [2.1.1.2 列出ADT](#2.1.1.2 列出ADT)
    • [2.1.1.3 具体实现](#2.1.1.3 具体实现)
    • [2.1.1.4 性能讨论](#2.1.1.4 性能讨论)

目录

【注】升级版本教程内容较多,因此对一些逻辑链较长的问题描述,我们往往会采用"step_x"的形式提醒读者当前讨论问题到了哪一步。

2.1.1.1 问题的出现

step_0 回忆数组的优缺点

  1. 优点:依靠index(索引),本质上依靠内存中的连续存储结构快速获取对应元素。
  2. 缺点:一旦声明就不能再更改大小。 ==> 可能存在空间的浪费 or 空间不足的情况。

step_1 于是,人们开始设想

如果能够动态把数据存储在数组中,制造这样一种工具:

  1. 不用顾忌元素个数超出存储空间上限(即:无限加入新的元素,自动扩容);
  2. 能够及时释放不使用的空间(即:删除元素后数组长度会适时缩减);
  3. 能够提供一些方便的函数操作(即:元素的增、删、改、查)。

那,岂不是一件美事?==> 于是乎,动态数组(ArrayList)诞生了。

(1)上述瞎想的过程其实蛮重要的,它将贯穿于整个教程笔记中,因为:任何一个数据结构的产生是有应用背景的!

(2)个人建议学习理工技术/知识前,一定要问自己三遍,问别人N遍:为什么要做这件事?想明白自己要做什么、在做什么?其优先级往往高于"怎么做",不然很容易陷入死板的学习旋涡,无限虚空内耗。

【注】在其他编程语言中,动态数组(ArrayList)还有另一个名字------向量(Vector),都是语法糖。

2.1.1.2 列出ADT

step_0 在实现一个具体的数据结构前,我们需要定义好这个数据结构,想清楚两个基础问题。

【Q1】如何存储数据?

【Q2】提供哪些操作?

上述描述有一个专门的术语:抽象数据类型(Abstract Data Type,ADT)。

【习惯】数据结构的具体实现方式是根据ADT展开的,实现形式比较自由;可以将ADT理解为一套接口标准,举个例子:

无论是哪一家生产厂家出品的DDR4内容,都可以插入到支持DDR4的主板上。

step_1 先画一张图,思考应该做些什么,不会写的代码先用中文代替。

c 复制代码
       struct ArrayList {
             (1)自定义类型的数组 data[]; //Q1
             (2)当前元素个数  len或者size;//Q2
             (3)数组最大的容量capacity;//Q3
       }

step_2 才开始我们就遇到了几个问题。

【Q1】这里的数组是否要用动态的?

【A1】用,直接用指针动态分配内存;在具体实现中会给出相关学习/讨论链接。

【Q2】当前元素个数len是十分重要的一个标记量,相比于当前最后一个元素的数组索引位置last,哪一个更好呢?

【A2】其实都差不多,len = last + 1,先用len,很多场合都希望得到当前元素个数。

【Q3】容量,要不要放在struct中作为标配?

【A3】都用到动态分配了,那必然是标配了;只不过是给定一个默认值,可拓展成更多的使用模式,例如:

(1)用户可指定生成的ArrayList的原始大小。

(2)用户可以不指定生成的ArrayList的原始大小,直接使用默认值。

【Q4】如何合理设计操作呢?

【A4】 先列一些基本的功能,做自己有把握很快能实现的,那些高大上的功能后面慢慢磨;细想数组元素的操作,无外乎"增、删、改、查 "(CRUD),所以基本功能应该围绕这4个点来讨论。

(1)生成一个ArrayList,最好是带*的,方便函数传参修改内容。

(2)("增" ,add)向当前list中尝试添加一个元素,分为默认在末尾添加和指定位置添加。

(3)("查" ,find/search)在当前list中尝试寻找给定元素item,找到了就返回该元素的位置(实际上是第一次出现的位置)。

(4)("删" ,delete)在当前list尝试删除元素,分为尝试删除给定元素和尝试删除给定位置的元素。

(5)("改" ,update)尝试修改当前list中指定位置的元素。

(6)获取给定位置元素:利用数组的优势,根据索引(index)找到对应的数据。

...

以上就是咱们的ADT了,注释贴在头文件ArrayList.h中,对照着写代码。

c 复制代码
typedef int ElementType;
#ifndef _Array_List_h
#define _Array_List_h

#define DEFAULT_CAPACITY (20)
#define OK (0)
#define ERROR (-1)

struct ArrayList{
	ElementType *data;   // 自定义类型的数组 data[]
	int len;             // 当前元素个数 len 或者 size
	int capacity;        // 数组最大的容量 capacity
};
typedef struct ArrayList *PtrArrayList;
typedef PtrArrayList List;
//(1)生成一个ArrayList,最好是带*的,方便函数传参修改内容。
List createList();
//(2)向当前list中尝试添加一个元素item,分为默认在末尾添加和指定位置pos添加
int addItem(List list, ElementType item, int pos);
int addItemTail(List list, ElementType item);
//(3)在当前list中尝试寻找给定元素item,找到了就返回该元素的位置(实际上是第一次出现的位置)
int findItem(const List list, ElementType item);
//(4)在当前list尝试删除元素,分为尝试删除给定元素item和尝试删除给定位置pos的元素
int removeItem(List list, ElementType item);
int removeByIndex(List list, int pos);
//(5)尝试修改当前list中指定位置pos的元素item
int setItem(List list, ElementType item, int pos);
//(6)尝试获取给定位置pos的元素
ElementType getItem(List list, int pos);
//(7)检测动态数组内是否没有元素
int isEmpty(List list);
// 可能还有别的功能。。。
#endif

需要注意几个点,代码规范些:

(1)为了防止后头文件被重复引用,用了

c 复制代码
#ifndef _Array_List_h
#define _Array_List_h
#endif

的常规处理方式

(2)typedef用于变量名称替换,翻车点(编译问和理解知识点),可以自行百度;通过

c 复制代码
typedef struct ArrayList *PtrArrayList;
typedef PtrArrayList List;

操作可隐去部分指针操作,typedef只是为了封装指针,隐藏细节,帮助大家更好的理解核心知识;typedef的写法有很多,这里仅仅是拆开写帮助基础薄弱的同学理解,我更喜欢使用匿名结构体。

(3)定义宏一定要记得用"()"护体,用" / "折行。

(4)先定两个状态量方便后面用,后续章节就不会那么死板了。

c 复制代码
#define OK (0)
#define ERROR (-1)

(5)很多函数把List作为传参,实际上将对应的内存喂给函数,既是入参也是出参!

【e.g.】initList(&list),我个人不建议这样写,参考流行编程语言的做法更好,都是语法糖。

(6)易读性强的代码就是最好的注释,强迫自己尽快适应,别傻乎乎地每一句都写注释,那样做你永远学不会编程!

【e.g.】积累函数命名方式:

增:insertXXX / addXXX

删:removeXXX/delXXX

改:setXXX/updateXXX

查:findXXX/searchXXX/contains

(7)传统的的C模块封装,都是将ADT声明放在.h头文件,具体实现放在.c文件中的;可能有读者会嘲笑这段话,但这两年我是亲眼目睹很多初学者被AI带歪了。复杂系统的结构设计和文件布局是有讲究的,模块化设计思想是基础;无脑用AI比不会用AI更可怕:

a.h 头文件对外部暴露模块能力;.c 文件对外隐藏模块具体实现。

b.用户在使用模块的时候,需要include .h头文件,但一般情况下并不关心其具体实现,这就是"封装"。

c.测试模块功能时才会写main()。 ⇒ 原谅一个心态被搞崩过的人的啰嗦。

具体工程示例:

2.1.1.3 具体实现

ArrayList.c 对照.h头文件实现每个功能, 挨个实现:写一段、编一段,测一段。

【注】很多时候数据结构的实现就像搭积木一样简单,不要以讹传讹认为它很难。

step_1 createList( ),返回一个可用的,没有元素的(即空的、全新的)动态数组。

【注】为了方便C语言乱学的朋友,加入了插入头文件的代码片段,后续贴中会省略。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ArrayList.h"

List createList(){
	List L = (PtrArrayList)malloc(sizeof(struct ArrayList));
	if(L == NULL){
		printf("Out of memery, create List fail\n");
		return NULL;
	}
	
	L->data = malloc(sizeof(ElementType) * DEFAULT_CAPACITY);
	if(L->data == NULL){
		printf( "Out of memery, create list array fail\n" );
	    free(L);
	    return NULL;
	}
	memset(L->data, 0, sizeof(ElementType) * DEFAULT_CAPACITY);

	L->capacity = DEFAULT_CAPACITY;
	L->len = 0;

	return L;
}

此处两个讨论点:

【Q1】malloc前面到底要不要加(xxx *)的强制转换?

【A1】看编译环境,有的编译器很厉害自动会帮转化,有的比较严格,视情况而定。

【Q2】memset到底要不要做?

【A2】同上;我建议还是要做的,毕竟不希望出现"烫烫烫",C/C++变量声明一定要初始化,这是好多企业的血泪教训。

若不考虑分配内存失败,可使用如下代码替代:

c 复制代码
L->data = (ElementType*)calloc(DEFAULT_CAPACITY, sizeof(ElementType));

【Q3】struct中的"."与"->"两种操作如何抉择?

【A3】同上,一般来讲 struct/*struct 是有区别的,看编译器。

【Q4】使用完free(ptr)之后,是否需要将ptr = NULL?

【A4】个人认为要看具体的场景和语言,给出几篇参考:

a.【C语言】5. 指针free后为什么要刻意指向NULL、野指针(原因、解决)、悬垂指针

b. 为什么 C 语言的 free 函数最后不自己把指针指向 NULL? ⇒ 经典话语:"在有经验的程序员看来,你这个蠢操作把错误隐藏的更深、导致程序逻辑更复杂、更难排除错误了"

c. 内存管理:C语言中的Malloc/free是如何分配内存的

【建议】别着急往下编,先来个main()试一把createList(),及时排错,main.c:

c 复制代码
int main(int argc, char *argv[]) {
	int i;
	List list = createList();
	printf("\nlist is empty? %d\n", isEmpty(list));
	return 0;
}

⇒ 即便你跟着我们的教程抄代码,作为初学者我们建议一段一段的抄写和测试,这样能够快速提升你对代码的理解、编写能力和编译排错能力。

step_2 int isEmpty(List list),用于检测当前list是否完成初始化(指针不为NULL,data[]没有数据,len=0)。

c 复制代码
int isEmpty(List list){
	if (list == NULL)
		return ERROR;
	return (list->len == 0);
}

step_3 int addItem(List list, ElementType item, int pos),在指定位置添加元素。

画个图一切明了:

a.在指定位置插入元素,需将所有元素向后挪一位;

b.注意最后一个元素的角标处理和元素个数增加(len++);

c. 还要考虑3种无效/非法操作

1)list为空;

2)数组满了 len == capacity;

3)插入位置越界(pos<0 or pos>=capacity)。

c 复制代码
int addItem(List list, ElementType item, int pos){
    int i;
    if (list == NULL) {
 	    printf("\nlist is null\n");
 	    return ERROR;
    } else if (pos < 0 || pos > list->capacity ) {
        printf("\npos out of range!\n");
        return ERROR;
    }
 
    if (list->len == list->capacity && growArray(list) == ERROR) {
 	    printf("\ngrow list err\n");
 	    return ERROR;
     }
 
     for (i = list->len-1; i >= pos; i--)
 	    list->data[i+1] = list->data[i];
 	    
     list->data[pos] = item;
     list->len++;
 
 	 return OK;	
}

此处有几个讨论点:

【Q1】非法判断那么多条件,要不要写成一个函数呢?

【A1】我认为是可行的,最好是看已经成型的开源库中他人的写法,适度即可。

【Q2】位置的循环操作中,到底是从最后一位last索引开始迭代,还是从pos索引开始迭代呢?

【A2】选择从后面开始比较妥当,处理起来方便很多,不相信可以尝试从pos迭代。

【注1】如下代码使用了逻辑短路技巧,此处的 growArray(list) 指的就是具体的扩容实现

c 复制代码
if (list->len == list->capacity && growArray(list) == ERROR)

growArray参考实现如下:

c 复制代码
int growArray(List list){
	int i;
	int resize = list->capacity << 1; //左移一位操作,相当于*2
	ElementType *newData = malloc(sizeof(ElementType) * resize);  // 理解resize的含义
	if(newData == NULL){
		printf("\ngrow array fail!\n");
		return ERROR;
	}
	memset(newData, 0, sizeof(ElementType) * resize);
	
	ElementType *tmp = list->data;
	for(i = 0; i < list->len; i++)
		newData[i] = list->data[i];
		
	list->data = newData;
	free(tmp);
	
	list->capacity = resize;
	return OK;
}

对于 默认添加元素方法addItemTail(),直接使用现有addItem()

c 复制代码
int addItemTail(List list, ElementType item){
 return addItem(list, item, list->len);
}

step_4 find操作,有的ADT里也称为:包含(contains)

c 复制代码
int findItem(const List list, ElementType item){
    int i = 0;
    if(list == NULL) return ERROR;
//	for(i = 0; i < list->len; i++){
//		if(list->data[i] == item)
//			return i;
//	}
//	return ERROR;
    while(i < list->len && list->data[i] != item)
 	    i++;
 
    return i > (list->len - 1) ? ERROR : i;
}

(1)注意const的用法,不希望list内容被修改。

(2)此处循环使用while/for皆可。

step_5 remove操作,同样需要挪位置。

【注】当前动态数组是允许有重复元素的。

c 复制代码
int removeItem(List list, ElementType item){
   int i;
   if(list == NULL){
 	  printf("\nlist is null\n");
 	  return ERROR;
   }
 
   int pos = findItem(list, item);
   if(pos != ERROR){
   	   for(i = pos; i < list->len-1; i++)
 	       list->data[i] = list->data[i+1];
 	       
 	   list->data[list->len-1] = 0;
 	   list->len--;
 	   return pos;
   }
 
   return ERROR;
}

同理,可以删除指定位置的元素。

c 复制代码
int removeByIndex(List list, int pos){
	int i;
	if(list == NULL){
		printf("\nlist is null\n");
		return ERROR;
	} else if(pos < 0 || pos >= list->len){
		printf("\npos out of range!\n");
		return ERROR;
	}
	
	for(i = pos; i < list->len-1; i++)
		list->data[i] = list->data[i+1];
		
	list->data[list->len-1] = 0;
	list->len--;
	
	return OK;
}

【注】

a. 如果不考虑输入异常,这两个功能是可以部分代码复用的;

b. 特别对于尝试删除元素的操作,其实是删除了list中第一个出现的该元素,我们的list是允许有重复元素存在的,这个操作的第一步就是通过find函数找到目标元素的索引位置。

step_7 set操作,替换掉指定位置(pos--position,index)的元素。

c 复制代码
int setItem(List list, ElementType item, int pos){
    if (list == NULL) {
 	    printf("\nlist is null\n");
 	    return ERROR;
    } else if (pos < 0 || pos >= list->len) {
        printf("\npos out of range!\n");
 	   return ERROR;
    }
 
    list->data[pos] = item;
    return OK;
}

step_8 get操作,获取指定位置的元素

c 复制代码
ElementType getItem(List list, int pos){
	if(list == NULL){
		printf("\nlist is null\n");
		return ERROR;
	} else if (pos < 0 || pos >= list->len) {
		printf("\npos out of range!\n");
		return ERROR;
	}
	
	return list->data[pos];
}

step_9 其他操作,例如遍历打印整个动态数组中的所有元素。

c 复制代码
void printList(const List list){
     int i;
     if (list != NULL) {
 	    printf("\n[ ");
 	    for (i = 0; i < list->len; i++)
 		    printf("%d ", list->data[i]);
 	
 	    printf("]\n");
     }
}

【ArrayList.c完整实现】

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ArrayList.h"

List createList(){
     List L = (PtrArrayList)malloc(sizeof(struct ArrayList));
     if (L == NULL) {
 	    printf("Out of memery, create List fail\n");
 	    return NULL;
     }
 
     L->data = malloc(sizeof(ElementType) * DEFAULT_CAPACITY);
     if (L->data == NULL) {
 	     printf( "Out of memery, create list array fail\n" );
          free(L);
         return NULL;
    }
    memset(L->data, 0, sizeof(ElementType) * DEFAULT_CAPACITY);

    L->capacity = DEFAULT_CAPACITY;
    L->len = 0;

    return L;
}

int growArray(List list){
    int i;
    int resize = list->capacity << 1;
    ElementType *newData = malloc(sizeof(ElementType) * resize);
    if (newData == NULL) {
        printf("\ngrow array fail!\n");
 	   return ERROR;
    }
    memset(newData, 0, sizeof(ElementType) * resize);
 
    ElementType *tmp = list->data;
    for (i = 0; i < list->len; i++)
 	    newData[i] = list->data[i];
 
    list->data = newData;
    free(tmp);
 
    list->capacity = resize;
    return OK;
}

int addItem(List list, ElementType item, int pos){
    int i;
    if (list == NULL) {
        printf("\nlist is null\n");
        return ERROR;
    } else if (pos < 0 || pos > list->capacity ) {
 	    printf("\npos out of range!\n");
 	    return ERROR;
    }
 
    if (list->len == list->capacity && growArray(list) == ERROR) {
        printf("\ngrow list err\n");
 	    return ERROR;
    }
 
    for (i = list->len-1; i >= pos; i--)
 	    list->data[i+1] = list->data[i];
 	    
    list->data[pos] = item;
    list->len++;
 
    return OK;	
}

int addItemTail(List list, ElementType item){
    return addItem(list, item, list->len);
}

int findItem(const List list, ElementType item){
    int i = 0;
    if (list == NULL) return ERROR;

    while (i < list->len && list->data[i] != item)
 	    i++;

    return i > (list->len - 1) ? ERROR : i;
}

int removeItem(List list, ElementType item){
    int i;
    if (list == NULL) {
        printf("\nlist is null\n");
 	    return ERROR;
    }
 
    int pos = findItem(list, item);
    if (pos != ERROR) {
        for(i = pos; i < list->len-1; i++)
 	        list->data[i] = list->data[i+1];
 	    
 	    list->data[list->len-1] = 0;
 	    list->len--;
 	    return pos;
    }
 
    return ERROR;
}

int removeByIndex(List list, int pos){
    int i;
    if (list == NULL) {
    	printf("\nlist is null\n");
    	return ERROR;
    } else if (pos < 0 || pos >= list->len) {
 	    printf("\npos out of range!\n");
 	    return ERROR;
    }
 
    for (i = pos; i < list->len-1; i++)
 	    list->data[i] = list->data[i+1];
 	    
    list->data[list->len-1] = 0;
    list->len--;
    return OK;
}

int setItem(List list, ElementType item, int pos){
    if (list == NULL) {
 	    printf("\nlist is null\n");
 	    return ERROR;
    } else if(pos < 0 || pos >= list->len) {
 	    printf("\npos out of range!\n");
 	    return ERROR;
    }
 
    list->data[pos] = item;
    return OK;
}

ElementType getItem(List list, int pos){
    if(list == NULL){
 	    printf("\nlist is null\n");
 	    return ERROR;
    } else if(pos < 0 || pos >= list->len) {
 	    printf("\npos out of range!\n");
 	    return ERROR;
    }
 
    return list->data[pos];
}

int isEmpty(List list){
    if(list == NULL) return ERROR;

    return (list->len==0);
}

void printList(const List list){
    int i;
    if (list != NULL) {
        printf("\n[ ");
 	    for (i = 0; i < list->len; i++)
 		    printf("%d ", list->data[i]);
 
 	    printf("]\n");
     }
}

step_10 测试代码(main.c),建议使用AI辅助完成

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include "ArrayList.h"

int main(int argc, char *argv[]) {
   int i = 0;
   printf("\n==============test create list && add item===================\n");
   List list = createList();
   printf("\nlist is empty? %d\n", isEmpty(list));
   
   for(i = 0; i < 10; i++)
   	addItemTail(list, i);
   
   printf("\ninit data: len=%d\n", list->len);
   printList(list);
   
   for(i = 20; i < 40; i++)
   	addItemTail(list, i);
   
   printf("\ngrow data: len=%d\n", list->len);
   printList(list);
   	
   addItemTail(list, 100);
   printList(list);
   addItem(list, 200, 3);
   printList(list);
   addItem(list, 77, 0);
   printList(list);
   addItem(list, 99, list->len);
   printList(list);
   addItem(list, 99, -1);
   printList(list);
   
   printf("\n==============test find item===================\n");
   printf("\nfind 77'pos=%d\n", findItem(list, 77));
   printf("\nfind 100'pos=%d\n", findItem(list, 100));
   printf("\nfind 20'pos=%d\n", findItem(list, 20));
   printf("\nfind 99'pos=%d\n", findItem(list, 99));
   printf("\nfind 500'pos=%d\n", findItem(list, 500));
   
   printf("\n==============test remove item===================\n");
   printf("\nremove 10 =>pos=%d\n", removeItem(list, 10));
   printList(list);
   printf("\nremove 10 again =>pos=%d\n", removeItem(list, 10));
   printList(list);
   printf("\nremove 77 =>pos=%d\n", removeItem(list, 77));
   printList(list);
   printf("\nremove 100 =>pos=%d\n", removeItem(list, 100));
   printList(list);
   printf("\nremove pos=0, ret=%d\n", removeByIndex(list, 0));
   printList(list);	
   printf("\nremove pos=10, ret=%d\n", removeByIndex(list, 10));
   printList(list);	
   printf("\nremove pos=%d, ret=%d\n", list->len-1, removeByIndex(list, list->len-1));
   printList(list);	
   printf("\nremove pos=%d, ret=%d\n", list->len, removeByIndex(list, list->len));
   printList(list);
   
   printf("\n==============test set item===================\n");
   printf("\nset -2 pos=%d ~~~ %d \n", 0, setItem(list, 0, -2));
   printList(list);	
   printf("\nset -10 pos=%d ~~~ %d \n", list->len-1, setItem(list, -10, list->len-1));
   printList(list);
   printf("\nset -100 pos=%d ~~~ %d \n", 7, setItem(list, -100, 7));
   printList(list);
   printf("\nset -50 pos=%d ~~~ %d \n", -1, setItem(list, -50, -1));
   printList(list);
   printf("\nset -60 pos=%d ~~~ %d \n", list->len, setItem(list, -60, list->len));
   printList(list);
   
   printf("\n==============test get item===================\n");
   printf("get pos=>%d = %d\n", 0, getItem(list, 0));
   printf("get pos=>%d = %d\n", -1, getItem(list, -1));
   printf("get pos=>%d = %d\n", list->len-1, getItem(list, list->len-1));
   printf("get pos=>%d = %d\n", list->len, getItem(list, list->len));
   
   printf("\nlist is empty? %d\n", isEmpty(list));
   
   return 0;
}

step_11 一串追加问题

思考并尝试编码实现/求证下列问题:

【Q1】既然扩容问题可以解决了,当闲置空间过多时是否考虑缩容?

【A1】remove()操作视情况执行缩容。

【Q2】缩容沿用:"发现当前使用空间不足原始空间一半,则直接空间缩减一半"的策略,可行吗?

【A2】不好吧,见过《九品芝麻官》中方唐镜在作死边缘反复横跳吗?

【Q3】能否将扩容和缩容两种操作整合成一个函数:resize(List list, int newSize)?

【A3】审视自己最newSize的理解,用resize替换growArray,尽量依靠自己从零写出来,不要总依靠AI!

2.1.1.4 性能讨论

step_0 感性的讨论

(1)依靠数组索引,ArrayList在获取元素方面(也就是查的能力)速度是有保障的。

(2)但缺点也很明显:插入和删除成本高。

step_1 理性的讨论(借助数学期望分析)

(1)插入问题,假设插入点为i,则
E i = ∑ i = 1 n + 1 p i ( n − i + 1 ) E_{i}=\sum_{i=1}^{n+1}p_{i}(n-i+1) Ei=i=1∑n+1pi(n−i+1)

随机插入,等概率 p i = 1 n + 1 p_{i}=\frac{1}{n+1} pi=n+11

代入有(求和号内等差数列)
E i = 1 n + 1 ∑ k = 1 n + 1 ( n − i + 1 ) = n 2 E_{i}=\frac{1}{n+1}\sum_{k=1}^{n+1}(n-i+1)=\frac{n}{2} Ei=n+11k=1∑n+1(n−i+1)=2n

(2)删除问题,假设插入点为i,则
E d = ∑ i = 1 n q i ( n − i ) E_{d}=\sum_{i=1}^{n}q_{i}(n-i) Ed=i=1∑nqi(n−i)

随机删除,等概率
q i = 1 n q_{i}=\frac{1}{n} qi=n1

代入有(求和号内等差数列)
E d = 1 n ∑ i = 1 n ( n − i ) = n − 1 2 E_{d}=\frac{1}{n}\sum_{i=1}^{n}(n-i)=\frac{n-1}{2} Ed=n1i=1∑n(n−i)=2n−1

上述两种操作均有时间复杂度 O ( n ) O(n) O(n)

【后记】还可以翻看老版的相关贴,对比严版教材:
1-1 线性表 ArrayList 原始版本
1-2 线性表 ArrayList 升级版本
1-3 线性表 ArrayList 严版教材实现浅析

相关推荐
StandbyTime2 小时前
C语言学习-菜鸟教程C经典100例-练习28
c语言
Yupureki2 小时前
《算法竞赛从入门到国奖》算法基础:入门篇-离散化
c语言·数据结构·c++·算法·visual studio
散峰而望2 小时前
OJ 题目的做题模式和相关报错情况
java·c语言·数据结构·c++·vscode·算法·visual studio code
疋瓞2 小时前
C/C++查缺补漏《5》_智能指针、C和C++中的数组、指针、函数对比、C和C++中内存分配概览
java·c语言·c++
zc.ovo2 小时前
线段树优化建图
数据结构·c++·算法·图论
程序员-King.2 小时前
day140—前后指针—删除排序链表中的重复元素Ⅱ(LeetCode-82)
数据结构·算法·leetcode·链表
黎雁·泠崖2 小时前
Java数组进阶:内存图解+二维数组全解析(底层原理+Java&C差异对比)
java·c语言·开发语言
Remember_9932 小时前
【JavaSE】一站式掌握Java面向对象编程:从类与对象到继承、多态、抽象与接口
java·开发语言·数据结构·ide·git·leetcode·eclipse
皮蛋sol周2 小时前
嵌入式学习数据结构(二)双向链表 内核链表
linux·数据结构·学习·嵌入式·arm·双向链表