线性表

线性表有头有尾穿成一串。

顺序表

顺序存储的线性表称为顺序表(Sequential List)。顺序存储指的是在线性表在存储空间内连续存放,这种存储方式就是顺序存储结构,逻辑上相邻的元素在物理存储上也相邻。

数组就是一种顺序表,定义一个顺序表结构:

/* 最大长度 */
#define MAXSIZE 100

/* 元素数据类型 */
typedef int element_t;

/* 顺序表类型 */
struct _sq_list{
    element_t *base;    /* 顺序表基地址 */
    int length;         /* 表长 */
};

typedef struct _sq_list *sq_list_t;

/* 定义一个顺序表 */
sq_list_t sq_list;

这时候就有一个类型为sq_list_t的顺序表sq_list了,这是一个指针,在使用的时候还需要为其分配内存空间。可以用sq_list.base[i*sizeof(element_t)]来访问第i个元素。可以看出,求表长、判断表是否为空、等操作的时间复杂度都是 O(1)O(1) ,下面讨论顺序表主要操作实现。

注意:元素计数按照C语言习惯从0开始,但是长度length就是元素个数

初始化

顺序表的初始化:

  • 1.为顺序表分配内存空间

  • 2.将表的长度设置为0

代码里malloc()函数用于申请内存空间,大小需要我们进行计算,其返回类型为(void*),因此需要强制转换为目标类型。使用完毕要及时释放空间。

取值

从顺序表里取出第i个数据,从0开始计数。算法步骤:

  • 1.判断标号是否合理

  • 2.取值数据

查找

插入

在第i个位置插入新元素e,并让长度加1,这时候就要考虑搬动元素了。

算法步骤:

  • 1.判断插入位置是否合法

  • 2.判断顺序表是否已满

  • 3.搬动元素

  • 4.插入新元素

  • 5.表长+1

这个算法没有处理表的动态扩充,表长到达预设最大长度时,就无法插入新元素了。这个算法主要的时间耗费在移动元素上,假设插入n个元素具有的n+1个位置是等概率的,概率为 pi=1/(n+1)p_i = 1/(n+1) ,那么移动次数的期望值为:

A=i=0n1pi(ni)=n2A = \sum_{i=0}^{n-1} p_i (n-i) = \frac{n}{2}

平均时间复杂度为 O(n)O(n) 。或者简单一点考虑,最差情况下,n个数据需要移动n次,那么最坏时间复杂度为 O(n)O(n)

删除

删除是插入的逆操作,直接后面覆盖前面就行,在操作上没有明显的删除这个行为,算法步骤:

  • 1.判断删除位置是否合法

  • 2.移动元素

  • 3.表长减1

同样的这个算法的主要时间也是耗费在搬动元素上,最坏情况下n个元素需要搬动n-1次,那么时间复杂度为 O(n)O(n)

总结

顺序表读取很快,但是插入删除不快,并且是静态的。有时候我们想要更好的插入删除性能,并且长度可以动态增加,这时候就要设计一种新的数据结构了。

线性表的链式存储结构,这种东西叫链表。一个元素除了存储信息,还要存储前后信息,这两部分合起来称为节结点(node)。结点包含数据域指针域,根据一个结点所含的指针数、指针连接方式可以分为单链表、循环链表、双向链表、十字链表、邻接表、邻接多重表等。

单链表

一个单链表的结构如下图:

单链表结点的定义:

初始化

结点的数据类型为node_t,这里使用了一个数据类型别名为link_list_t,本质上是指向结点类型的一个指针,可以用来定义头指针,头指针需要初始化,或者叫作链表初始化,算法步骤为:

  • 1.头指针指向一个新的node_t类型的内存空间(新结点)

  • 2.新结点的指针设置为空

取值

取出第i个结点的数据,假设从0开始计数

这个算法的基本操作是比较ij并移动指针,最坏时间复杂度为 O(n)O(n) ,等概率读取下平均时间复杂度为

A=i=0n11ni=n12A = \sum_{i=0}^{n-1}\frac{1}{n} \cdot i = \frac{ n-1 }{2}

则平均时间复杂度为 O(n)O(n)

查找

查找给定元素是否在链表上,如果在则返回地址,否则最后返回了NULL

平均时间复杂度计算核上面差不多都为 O(n)O(n)

插入

在第i个位置插入一个新节点(从0开始计数)

删除

单向循环表

双向链表

顺序表和链表性能比较

应用

最后更新于