【数据结构】——顺序表的插入、扩容、删除、性能分析基础详解

 

1.1线性表的定义

        线性表:是由n个数据元素组成的有限序列, 记为 (a1,a2,……,ai,……,an)  

        eg:英文字母表(a,b,c……z)是一个长度为26的线性表

 

 问题1: 线性表是否允许长度n=0?

     答:是!(从实际出发,为什么要有数据结构?为什么要有线性表?——让我们存储元素的呀!当我们的元素还没有进入线性表的时候,n=0)

问题2: 什么叫前驱/后继?每个元素都有前驱/后继吗?

     答:前驱——此元素前面的一个元素就是它的前驱。

            后继——此元素后面的一个元素就是它的后继。

           每个元素可能有一个前驱和后继。最后一个元素没有后继,前面的元素都有唯一的后继,eg:a1的后继是a2。第一个元素没有前驱,其他元素都有前驱

 

 

1.2  线性表的顺序存储结构

 顺序表:用连续存储空间顺序存储数据元素的线性表

      元素地址要满足的关系:

    (1) Loc(ai)=Loc(a1)+(i-1)*m

    (2) Loc(ai+1)=Loc(ai)+m

 其中m是一个元素占用的存储单元个数,Loc(ai)是线性表的第i个元素的地址,第一个元素的地址也叫基地址

 

       顺序表的特点:

  1. 逻辑上相邻的元素,物理地址也相邻
  2. 高性能的随机存取,低性能的插入删除,高性能的顺序访问

 

为了插入元素的方便,我们还会开辟一段备用空间。表的容量是capacity。在实际的写代码时,会用一个数组来记录基地址

template < typename ElemType >
struct SeqList{
    ElemType * items;  //基地址指针
    int  length;       //顺序表长度
    int  capacity;     //顺序表容量
};

//定义好啦以后,要申请才可以真的使用

items = new ElemType [capacity];
//申请成功,会返回一个非空的指针,失败返回空指针

 

 

1.3 顺序表的插入元素

  接口: void insertBefore (int index, ElemType e)

  语义: 将元素e插入到顺序表第index个元素之前插

 

void insertBefore(int index,ElemType e){
    if(index <0|| index > length)    //在插入之前应该检查要插入的位置是否符合实际情况
        throw "out of range";          //如果插入位置小于0或大于表长,抛出异常
    for(int i=length; i>index; i--)    //从最后一个元素开始移动一个
        items[i] = items[i-1];
    items[index] = e;                  //插入元素e
    length++;                          //插入后长度+1
}

为了避免覆盖,从最后一个元素开始移动。直到移动叭ai的位置空出,插入元素e。

 

 

1.4 顺序表插入的性能分析

假设Pi是在第i个元素之前插入一个元素的概率,则在长度为n的线性表中插入一个元素时,移动元素的次数的平均次数为:

                                                                     E=\sum_{i=1}^{n+1}Pi(n-i+1)

E是期望,如何知道概率P?——在不知道其分布的情况下,可以假设其为均匀分布,    P=\frac{1}{n+1}    (长度为n,+1的1是可以在最后一个元素的后面插入元素)

                                                                  E=\frac{1}{n+1}\sum_{i=1}^{n+1}(n-i+1)=\frac{n}{2}

时间复杂度 T\left ( n \right )=O\left ( n \right ) ,由计算得,表长为n的顺序表平均要移动n/2次

 

 

1.5 顺序表的扩容

新建顺序表的时候,我们预留啦备用空间,当我们不断插入元素后,顺序表的备用空间被用完,将会导致不能再继续插入元素。此时要将顺序表扩容

 1.就近扩容法

free那段空间是操作系统知道是空闲的,但是用户并看不见。此时就要像操作系统递交申请新的空间。

新地址 = realloc(旧地址,新容量);           //申请新的容量以扩充顺序表
if (新地址 == NULL ) throw "申请空间失败";   //申请失败返空指针,则抛出异常
if (新地址 != 旧地址 ){
    memcpy (新地址, 旧地址, 旧容量);         //把原来的表的内容复制到新表
    free (旧地址);                          //释放旧表的空间
}

时间复杂度T\left ( n \right )=\theta \left ( 1 \right )

 

 

补补课:

realloc函数的使用

函数原型:void *realloc(void *p,size_t size)

功能:realloc函数将p指向的对象的长度修改为size个字节,如果新分配的内存比原来的大,则原来的数据保持不变,增加的空间不进行初始化。

memcpy函数的使用

函数原型:extern void *memcpy( void *dest,  void *src,  unsigned int count );

用法:#include <string.h>

功能:由src所指内存区域复制count个字节到dest所指内存区域。   

说明:src和dest所指内存区域不能重叠,函数返回指向dest的指针。

 

但是很不幸的是绝大多数情况都不能做到就近扩容,所以有下面的这个方法。

2.整体挪动法

后面的内存空间已经被别的占用啦,所以就不能就近扩容,只能整体的挪动。使用循环memcpy来把原有元素搬家到新的顺序表中,所以时间复杂度是T\left ( n \right )=\theta \left ( n \right )

 

这里都讨论啦扩容的方法,但是并没有讨论扩多大,又1.6来详解。

1.6 扩容的策略

  等量扩容

   从1个元素依次扩容到n个元素,每次扩容固定增加x个元素存储空间,因此需要扩容 \frac{n}{x} 次。例如,一次扩容8个存储空间,用完后又会再继续扩容8个存储空间……

    假设x = 1,分摊到每个元素插入时间复杂度为:    (分摊也是一种平均,所以式子里有 \frac{1}{n} )

                           T\left ( n \right )= \frac{1}{n}\left ( 1+2+3+ \cdot \cdot \cdot +n\right )= \frac{1}{n}\cdot \frac{n\left ( n+1 \right )}{2}=\theta \left ( n \right )

(解释:分摊到每次到插入,分摊到n次到扩容,x=1,每次都会导致一个扩容,因为用了刚扩的这个就没多的空间啦,就要扩容。第一个元素来了,放进去。第二个元素来了,要申请两个存储空间,把第一个元素复制放进去,第二个再写进去,做两次操作。第三个元素来了,要把前面两个元素写到新到空间,再把新插入的元素写进去。随着不断的扩容,消耗逐渐扩大,式子约掉以后发现是线性的复杂度 \theta \left ( n \right ) ——为了插入一个元素,平均扩容要移动n个元素,可以看出代价很大)

 

由刚刚的方式可以看出,等量扩容是个消耗很大的方式,因此提出了新的改进如下。

 

等比扩容

比例是2,每次扩容改为翻倍扩容。例如,第一次扩容1个存储空间,第2次扩容2个,第三次扩容8个,等比来扩容。

                              T\left ( n \right )= \frac{1}{n}\left ( 1+2+4+8+\cdot \cdot \cdot +n \right )= \frac{1}{n}\cdot \frac{1-2^{\log n }}{1-2}=\theta \left ( 1 \right )

和等量扩容的区别就是要加的项变少了,(1+2+4+…+n)是 \log n 项。复杂度变成常数复杂度,性能提高

 

1.7 删除元素

接口: void deleteAt(int index)

语义:   删除顺序表的第index个元素

void deleteAt (int index){
     if( index < 0 || index >= length )        //判断要删除的元素的位置
        throw "out of range";                  //不符合实际则抛出异常
     for( int i = index; i < length - 1; i++ ) //从index后面一个元素开始移动
        items[i] = items[i + 1];
    length--;                                  //删除元素,长度减1
}

 

1.8 顺序表删除的性能分析

假设Pi是删除第i个元素的概率,则在长度为 n 的顺序表中删除一个元素所需移动的元素次数的平均次数是:

                                                             E=\sum_{i=1}^{n} Pi\left ( n-i \right )

E是期望,有n个可以删除的位置,每个位置的概率是Pi,每次删除都会导致后面的 n - i 个元素发生移动(代价)。同样假设Pi服从均匀分布,所以 Pi=\frac{1}{n} ,计算得:

                                                       E=\frac{1}{n}\sum_{i=1}^{n}\left ( n-1 \right )= \frac{n-1}{2}

所以时间复杂度 T\left ( n \right )=O\left ( n \right )。由此看出,在顺序表中插入或删除一个元素时,平均移动表的一半元素,当n很大时,消耗很大,效率低。

  • 9
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
顺序表是一种基于数组的数据结构,可以实现随机访问和快速插入删除操作。以下是 C++ 实现顺序表插入删除的代码示例: ```c++ #include <iostream> using namespace std; const int MAX_SIZE = 100; // 定义最大长度 // 定义顺序表结构体 typedef struct { int data[MAX_SIZE]; // 存放数据 int length; // 当前长度 } SeqList; // 初始化顺序表 void InitList(SeqList &L) { L.length = 0; } // 插入元素 bool ListInsert(SeqList &L, int pos, int val) { if (pos < 1 || pos > L.length + 1) { return false; // 插入位置不合法 } if (L.length >= MAX_SIZE) { return false; // 顺序表已满 } for (int i = L.length; i >= pos; i--) { L.data[i] = L.data[i - 1]; // 依次后移 } L.data[pos - 1] = val; // 插入新元素 L.length++; // 长度加一 return true; } // 删除元素 bool ListDelete(SeqList &L, int pos) { if (pos < 1 || pos > L.length) { return false; // 删除位置不合法 } for (int i = pos; i < L.length; i++) { L.data[i - 1] = L.data[i]; // 依次前移 } L.length--; // 长度减一 return true; } // 输出顺序表 void PrintList(SeqList L) { for (int i = 0; i < L.length; i++) { cout << L.data[i] << " "; } cout << endl; } int main() { SeqList L; InitList(L); ListInsert(L, 1, 10); ListInsert(L, 2, 20); ListInsert(L, 3, 30); PrintList(L); // 输出 10 20 30 ListDelete(L, 2); PrintList(L); // 输出 10 30 return 0; } ``` 这里使用了结构体来定义顺序表,其中 `data` 数组存放数据, `length` 表示当前长度。`InitList` 函数用于初始化顺序表, `ListInsert` 函数用于插入元素, `ListDelete` 函数用于删除元素, `PrintList` 函数用于输出顺序表

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值