线性表
数据结构的框架如图:
线性表、栈和队列、串都是线性结构。
特点是:在数据元素的非空有限集中,
- 存在唯一一个被称为“第一个”的数据元素
- 存在唯一一个被称为“最后一个”的数据元素
- 除第一个外,集合中每个数据元素均只有一个前驱
- 除最后一个外,集合中每一个数据元素均只有一个后继
注:非空、有限
线性表由数据元素组成,数据元素是由数据项构成
1、顺序表
用一组地址连续的存储单元依次存放数据元素
元素物理地址 = 起始地址 + 存储单元大小*第i个元素
L
o
c
(
e
i
)
=
L
o
c
(
e
0
)
+
c
∗
i
Loc(e_i) = Loc(e_0) + c*i
Loc(ei)=Loc(e0)+c∗i
顺序表的操作
- InitList(Sqlist &L) 构造空的线性表
- CreateList(Sqlist &L, int L_count) 向表中填入内容
- PrintList(Sqlist L) 打印顺序表
- InsertList(Sqlist &L, int pos, int elem) 向顺序表第pos个位置插入元素elem
- DeleteList(Sqlist &L, int pos) 删除顺序表中位置pos的元素
- SortList(Sqlist L, int elem) 查找顺序表中的元素elem
1.1 顺序表的实现
// 构造空的线性表
int InitList(Sqlist &L){
L.elem = (int *)malloc(LIST_INIT_SIZE * sizeof(int)); // 申请表空间(大小=表长*字符大小)
if(! L.elem)
return 0;
L.length = 0;
L.listsize = LIST_INIT_SIZE;
}
// 向表中填入内容
void CreateList(Sqlist &L, int L_count){
int i;
for(i=0; i<L_count; i++){
printf("请输入元素:");
scanf("%d", &L.elem[i]);
L.length ++;
}
}
// 打印顺序表
void PrintList(Sqlist L){
int i;
for(i=0; i<L.length; i++){
printf("位置%d", i);
printf("元素%d\n", L.elem[i]);
}
printf("\n");
}
1.2 插入操作
// 向顺序表L第pos个位置插入元素elem
int InsertList(Sqlist &L, int pos, int elem){
int *newbase;
int *p, *q;
if(pos<0 || pos>L.length)
return 0;
if(L.length >= L.listsize){
newbase = (int *)realloc(L.elem, (L.listsize + LISTINCREMENT)*sizeof(int)); // 超过预设空间,重新申请空间
if(! newbase)
return 0;
L.elem = newbase;
L.listsize += LISTINCREMENT;
}
q = &(L.elem[pos]);
for(p=&(L.elem[L.length-1]); p>=q; --p)
*(p+1) = *p;
*q = elem;
++ L.length;
}
注:其中的realloc函数,参考这篇博文c语言中realloc()函数解析
*如果将分配的内存减少,realloc仅仅是改变索引的信息。
如果是将分配的内存扩大,则有以下情况:
1)如果当前内存段后面有需要的内存空间,则直接扩展这段内存空间,realloc()将返回原指针。
2)如果当前内存段后面的空闲字节不够,那么就使用堆中的第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,并将原来的数据块释放掉,返回新的内存块位置。
3)如果申请失败,将返回NULL,此时,原来的指针仍然有效。
也就是说成功操作后,所建为新指针,旧指针被抛弃无法再操作。
1.3 删除操作
// 删除顺序表中位置pos的元素
int DeleteList(Sqlist &L, int pos){
int *p, *q;
int i;
if(pos<0 || pos>L.length)
return 0;
p = &L.elem[pos]; // 被删除元素位置
q = L.elem + L.length - 1; // 表尾元素
for(p; p<q; p++){
*p = *(p+1);
}
--L.length;
}
1.4 查找操作
// 查找列表中的元素
int SortList(Sqlist L, int elem){
int i;
for(i=0; i<L.length; i++){
if(elem == L.elem[i]){
return i;
}
}
return -1;
}
在顺序查找这里有一个监督元技术,通过预设一个监督元,减少判断条件,获取效率
顺序表这里比较简单,主要注意它在插入和删除时会有大量的元素移动。不过查找上很快捷,可以参考C语言的数组操作。
2、普通链表
链表的种类如下
首先介绍一下普通链表的操作
- CreateList(LinkList &L, int L_count)建立单链表
- PrintList(LinkList L)打印链表
- InsertList(LinkList &L, int pos, int elem)向链表第pos个位置插入元素elem
- DeleteList(LinkList &L, int pos)删除链表中位置pos的元素
- SortList(LinkList L, int elem)查找列表中的元素
单链表结点定义:
typedef struct LNode{
ElemType data; //数据域
struct LNdode *next; //指针域
}
2.1 链表的实现
在建立链表时注意两点
- 带不带头结点
- 头插法or尾插法
头结点就是设置一个头部空结点,这里面不会存入数据值,只有NULL或者指向下一个结点。
优点在于:1、无论链表是否为空,头指针都指向头结点。
2、在链表的第一个位置上的操作和在表的其他位置上的操作一致。
在链表中设置头结点有什么好处?
- 便于首元结点的处理
- 便于空表和非空表的统一处理
// 使用不带头结点的尾插法建立单链表L
void CreateList(LinkList &L, int L_count){ //L_count为新建链表长
int i;
LNode *p, *last;
L = last = (LinkList)malloc(sizeof(LNode));
L = NULL; //此链表无头结点,空链表头指针L直接为空
if(L_count == 0)
return;
for(i=0; i<L_count; i++){
p = (LinkList)malloc(sizeof(LNode)); // 生成新结点
printf("请输入元素:");
scanf("%d", &p->data);
p->next = NULL;
if(L == NULL){
L = p;
last = p;
}
else{
last->next = p;
last = p;
}
}
}
// 使用带头结点的头插法建立单链表L
void CreateList(LinkList &L, int L_count){ //L_count为新建链表长
int i;
LNode *p;
L = (LinkList)malloc(sizeof(LNode)); //建立头结点L
L->next = NULL;
if(L_count == 0)
return;
for(i=0; i<L_count; i++){
p = (LinkList)malloc(sizeof(LNode)); // 生成新结点
printf("请输入元素:");
scanf("%d", &p->data);
p->next = L->next;
L->next = p;
}
}
操作过程可以见下图
所以头插和尾插区别不大,尾插需要标记一个尾结点标志last
使用区别上,主要是为了得到链表数据次序不同。
// 打印链表
void PrintList(LinkList L){
int i=0;
if(L == NULL){
printf("链表为空\n");
}
else{
while(L->next != NULL){
printf("位置%d,元素%d\n", i, L->data);
L = L->next;
i ++;
}
printf("位置%d,元素%d\n", i, L->data);
}
printf("\n");
}
2.2 插入操作
// 向链表第pos个位置插入元素elem
int InsertList(LinkList &L, int pos, int elem){
int i=0;
LNode *p;
p = (LinkList)malloc(sizeof(LNode));
p->data = elem;
if(pos == 0){
// 插入位置为链表首
p->next = L;
L = p;
}
else{
while(i<pos-1){
i ++;
L ++;
}
p->next = L->next;
L->next = p;
}
}
2.3 删除操作
// 删除链表中位置pos的元素
int DeleteList(LinkList &L, int pos){
int i=0;
LNode *p, *flag_node;
if(pos == 0){
// 被删除的是链表首
printf("被删除的元素是%d\n", L->data);
L = L->next;
}
else{
flag_node = p = (LinkList)malloc(sizeof(LNode));
flag_node = L;
while(i<pos-1){
i ++;
flag_node = flag_node->next;
}
p = flag_node->next;
printf("被删除的元素是%d\n", p->data);
flag_node->next = p->next;
}
}
2.4 查找操作
// 查找列表中的元素
int SortList(LinkList L, int elem){
int i=0;
while(L != NULL){
if(L->data == elem){
return i;
}
L = L->next;
i ++;
}
return -1;
}
3、特殊链表
3.1 双链表
双链表在单链表的结点中加入了一个前驱指针
单链表对于其前驱结点的访问需要O(n),增加一个前驱指针后,插入、删除结点的时间为O(1)
双链表的结点定义
typedef struct DNode{
ElemType data;
struct DNode *prior,*next; //前驱和后继指针
}DNode, *DLinkList;
下图为双链表插入操作
双链表的结点删除操作如下:
先进行①②,然后③free( p); //释放p结点空间
3.2 循环链表
- 循环单链表的最后一个结点的指针不是NULL,而是指向头结点。
- 循环单链表在任意位置插入、删除结点操作一样,无需判断是否为尾结点
- 循环双链表为空表时,头结点的prior和next指针域都是L
3.3 静态链表
静态链表采用数组形式来描述链表,一行数据分两块,一块放置数据,一块放置下一个数的数组下标。
静态链表常用于不支持指针的语言中
4、顺序表与链表优势与劣势
- 顺序表的缺点:在做插入或者删除操作时需要移动大量元素;静态存储,数据元素的个数不能自由扩充
- 顺序表最主要特点:随机存取,即通过首地址和元素序号可在时间O(1)内找到指定元素
- 链表的优点:不需要预先确定链表的大小,是一种动态结构(需要多少加多少)
- 链表的缺点:在进行查找操作和求链表长度操作时麻烦,失去了顺序表可以随机存取的优点
小狼的相关博文:
-
数据结构和算法系列代码测试代码