线性表的基本概念
从今天开始我们正式进入数据结构的学习,开始之前我们先思考几个问题。
生活中遇到哪些线性表的例子?
如何通过程序实现线性表?
线性表结构有哪些基本操作?
线性表结构在软件方面有哪些应用?
看着上面的表情包是不是有种熟悉的味道,没错,这就是我们生活中最常见的线性表结构——糖葫芦。根据它的结构特性我们给出相应的定义,一个线性表,由有限个具有相同性质的元素构成,结构上要求,非表头和表尾元素有且只有一个前驱和后继。对于表头和表尾元素,如果是没有环的线性表,则表头元素没有前驱,表尾元素没有后继。如果有环则均存在前驱和后继元素,如下图所示。
线性表的实现
线性表的实现,根据表元素存储方式的不同,可以分为两种。一种是线性表中的所有元素占用一片连续的存储空间,我们称之为顺序存储,一种是表中的所有元素不需要占据连续的空间,当表中插入新的元素的时候,额外申请元素所需要的空间,插入到线性表中即可,我们称之为链式存储,下面我们通过程序来依次实现以上两种存储方式。
- 顺序表的实现(假设顺序表起始地址为b,每个元素占据内存为l)
根据上图所示,显然我们可以使用数组来表示一个顺序表结构,具体如下所示:
// 使用数组来表示顺序表结构
#define MAXSIZE 100 // 顺序表最大长度
typedef int ElemType
typedef struct {
ElemType data[MAXSIZE];
int len // 顺序表长度
}SqList;
使用数组来表示顺序存储结构有一个问题,就是在程序的执行过程中其能表示的元素的个数最大值是不可修改的,如果顺序表元素较少,则存在存储空间的浪费,如果顺序表元素比较多,则有可能导致顺序表的存储空间不够。基于这个问题,我们可以使用动态分配来实现顺序表结构,当内存空间不够用的时候可以重新分配一块更大的内存,并把当前数据拷贝到新的内存当中,具体操作请看后面的基本操作。
// 通过动态分配内存来实现顺序表结构
#define INIT_SIZE 100
#define INCREMENT 20
typedef int ElemType
typedef struct {
ElemType *data;
int len; // 顺序表长
int cap; // 顺序表最大容量
}SqList;
- 链式表(链表)实现
链式表相比于顺序表,不要求整个表分配在一块连续的存储空间,这对于占用内存比较大的数据来说比较方便,具体实现如下所示。
// 使用链表实现线性表结构
typedef int ElemType
typedef struct LNode {
ElemType data;
structLNode *next;
}LNode, *LinkList;
假设 L 是 LinkList 类型的变量,为了操作方便我们通常使用该变量的指针域作为链表的第一个节点,L 称之为链表的头节点,对链表判空直接看 L->next == NULL ? empty : nonempty 即可。
线性表的基本操作
- 数组实现的顺序表
// 初始化、插入、更新、删除操作
#define MAXSIZE 100 // 顺序表最大长度
typedef int ElemType;
typedef struct {
ElemType data[MAXSIZE];
int len // 顺序表长度
}SqList;
bool init_sq_1(SqList *sq) {
if (NULL == sq) {
return false;
}
sq->len = 0;
return true;
} // init_sq_1
bool insert_sq_1(SqList *sq, int pos, ElemType data) {
// 判断顺序表是否为空,顺序表是否已满,pos是否合法
if (sq == NULL || pos < 0 || pos > sq->len || sq->len == MAXSIZE) {
return false;
}
int i = sq->len;
for ( ; i > pos; i--) {
sq->data[i] = sq->data[i-1];
}
sq->data[pos] = data;
sq->len++;
return true;
} // insert_sq_1
bool update_sq_1(SqList *sq, int pos, ElemType data) {
// 判断顺序表是否为空,pos是否合法
if (NULL == sq || pos < 0 || pos > sq->len || pos == MAXSIZE ) {
return false;
}
// pos == sq->len 直接做插入操作
if (pos == sq->len) {
return insert_sq_1(sq, pos, data);
}
sq->data[pos] = data;
return true;
} // update_sq_1
bool delete_sq_1(SqList *sq, int pos, ElemType *value) {
// 判断顺序表是否为空,pos是否合法
if (NULL == sq || pos < 0 || pos >= sq->len) {
return false;
}
int i = pos;
*value = sq->data[pos];
for (; i < sq->len-1; i++) {
sq->data[i] = sq->data[i+1];
}
sq->len--;
return true;
} // delete_sq_1
- 通过动态分配内存来实现的顺序表结构
// 初始化、插入、查找、删除操作
#define INIT_SIZE 100
#define INCREMENT 20
typedef int ElemType;
typedef struct {
ElemType *data;
int len; // 顺序表长
int cap; // 顺序表最大容量
}SqList;
bool init_sq_2(SqList *sq) {
if (NULL == sq) {
return false;
}
sq->data = (ElemType*)malloc(sizeof(ElemType)*INIT_SIZE);
if (NULL == sq->data) {
return false;
}
sq->len = 0;
sq->cap = INIT_SIZE;
return true;
} // init_sq_2
bool insert_sq_2(SqList *sq, int pos, ElemType data) {
// 判断顺序表是否为空,pos是否合法
if (NULL == sq || pos < 0 || pos > sq->len) {
return false;
}
// 当前动态数组已经满了,插入新元素需要重新申请一块内存
int i = 0;
ElemType *newData = NULL, *tmpData = NULL;
if (sq->len == sq->cap) {
tmpData = (ElemType*)malloc(sizeof(ElemType)*(sq->cap+INCREMENT));
if (tmpData == NULL) {
return false;
}
newData = tmpData;
for (i = 0; i < sq->len; i++) {
*tmpData++ = sq->data[i];
}
free(sq->data);
sq->data = newData;
sq->cap += INCREMENT;
}
// 插入新元素到中间位置
for (i = sq->len; i > pos; i--) {
sq->data[i] = sq->data[i-1];
}
sq->data[pos] = data;
sq->len++;
return true;
} // insert_sq_2
bool update_sq_2(SqList *sq, int pos, ElemType value) {
// 判断顺序表是否为空,pos是否合法
if (NULL == sq || pos < 0 || pos >= sq->len) {
return false;
}
sq->data[pos] = value;
return true;
} // update_sq_2
bool delete_sq_2(SqList *sq, int pos, ElemType *value) {
// 判断顺序表是否为空,pos是否合法
if (NULL == sq || pos < 0 || pos >= sq->len) {
return false;
}
int i = pos;
*value = sq->data[pos];
for (i = pos; i < sq->len; i++) {
sq->data[i] = sq->data[i+1];
}
sq->len--;
return true;
}
- 使用链表实现的线性表结构
// 初始化、插入、查找、删除操作
typedef struct LNode {
// 为了操作方便,头节点的data用于记录链表的长度
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool init_link_list(LinkList L) {
if (NULL == L) {
return false;
}
L->data = 0;
L->next = NULL;
return true;
} // init_link_list
bool insert_link_list(LinkList L, int pos, ElemType data) {
// 判断链表是否为空,pos是否合法
if (L == NULL || pos < 0 || pos > L->data) {
return false;
}
int i = 0;
LNode *tmpNode = L->next;
LNode *newNode = NULL, *prior = L;
// 创建新节点
newNode = (LNode*)malloc(sizeof(LNode));
if (newNode == NULL) {
return false;
}
newNode->data = data;
newNode->next = NULL;
// 确定插入节点位置
while (i++ < pos && tmpNode != NULL) {
prior = tmpNode;
tmpNode = tmpNode->next;
}
prior->next = newNode;
newNode->next = tmpNode;
L->data ++ ;
return true;
} // insert_link_list
bool update_link_list(LinkList L, int pos, ElemType data) {
// 判断链表是否为空,pos是否合法
if (L == NULL || pos < 0 || pos >= L->data) {
return false;
}
int i = 0;
LNode *tmpNode = L->next;
while (i++ < pos) {
tmpNode = tmpNode->next;
}
tmpNode->data = data;
return true;
} // update_link_list
bool delete_link_list(LinkList L, int pos, ElemType *data) {
// 判断链表是否为空,pos是否合法
if (L == NULL || pos < 0 || pos >= L->data) {
return false;
}
int i = 0;
LNode *tmpNode = L->next;
LNode *prior = L;
while (i++ < pos && tmpNode != NULL) {
prior = tmpNode;
tmpNode = tmpNode->next;
}
*data = tmpNode->data;
prior->next = tmpNode->next;
free(tmpNode);
L->data--;
return true;
} // delete_link_list
应用
- 设将 n (n > 1) 个整数存放到一维数组中,设计一个再时间和空间两方面都尽可能高效的算法,将 R 中保存的序列循环左移 p (0<p<n) 个位置,即将 R 中的数据由(X0, X1, X2 … Xn) 变换为(Xp, X(p+1), …, X0, X1, …, X(p-1))。
- 解析图示
从下图我们可以看出,要实现一维数组的左移操作,我们可以先将将整个数组进行reverse,然后将数组分成两部分分别进行元素reverse,第一部分为前n-p个元素,第二部分为后p个元素,reverse 结束最终实现数组左移操作。
- 示例代码
bool ShiftLeft(SqList *SQ, int left, int right) {
int tmpData = 0;
if (left > right || left < 0 || right > SQ->len-1) {
printf("left %d or right %d invalid \n", left, right);
return false;
}
// reverse table
while (left < right) {
tmpData = SQ->data[left];
SQ->data[left] = SQ->data[right];
SQ->data[right] = tmpData;
left++;
right--;
}
return true;
}
- 判断链表是否存在环
- 解析
我们定义两个指针,一个 fast 指针,一个 low 指针。两个指针初始位置都是链表的头节点,分别移动这两个指针,fast 指针每次移动两个位置,low 指针每次移动一个位置。下图中我们可以看到,如果链表存在环,fast 指针和 low 指针必然相遇。如果链表没有环,则 fast 必然先指向 NULL。
- 示例代码
bool CircularLinkedList(LinkList L) {
if (L == NULL) {
// 链表为空
return false;
}
// 初始位置设为头节点
LinkList slow = L->next;
LinkList fast = L->next;
while (slow != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
return true;
}
}
// 说明 slow == NULL || fast->next == NULL
// 没有环
return false;
}