线性表

2.1 线性表的定义和特点

数据特性相同 有限序列

2.2 案例

一元多项式

稀疏多项式:在所有系数都不为零的情况下 需要多一倍的存储空间 但在一般情况下是省空间的

2.3 线性表的类型定义

2.4 线性表的顺序表示和实现

2.4.1 线性表的顺序存储表示

需要地址连续的存储单元

逻辑上相邻的数据元素在物理次序上也相邻

2.5.1 单链表的定义和表示

存储结构(物理位置)可以不连续。(非顺序映像/链式映像)

typedef struct LNode {
	ElemType data; // 数据域
	struct LNode *next; // 指针域
} LNode, *LinkList; // (同一结构体指针类型起了两个名称)
// LinkList是指向结构体LNode的指针类型
// 如果定义LinkList L 则L就是单链表的头指针
// 如果定义LNode *p 则p就是指向单链表中某个结点的指针

每个结点的指针域指向其直接后继结点。

单链表由表头指针唯一确定,所以可以用表头指针的名字为单链表命名。

头结点(不作为表长计入 头结点的指针域指向首元结点
首元结点 链表中存储第一个元素的结点
头指针 指向链表中第一个结点的指针

加入头结点的好处:

  1. 首元结点可以和其他结点同样处理。

  2. 判定空表:L == NULL

头指针始终是指向头结点的非空指针。

单链表是顺序存取 意思是只能从头开始遍历

2.5.2 单链表基本操作的实现

  1. 初始化

    Status IntList(LinkList &L) {
    	L = new LNode;
        L -> next = NULL; // 头结点指针域置空
        return OK;
    }
    
  2. 取值

    只能从头到尾遍历。

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

    Status GetElem(LinkList L, int i, ElemType &e) {
    	p = L-> next; 
    	j = 1; // 计数器
    	while(p && j < i) {
    		p = p -> next;
    		++j;
    	}
    	if(!p || j > i) return ERROR;
    	e = p -> data;
    	return OK;
    }
    
  3. 查找

    从头到尾查找。

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

  4. 插入

    Status ListInsert(LinkList &L, int i, ElemType e) {
    	// 在带头结点的链表L中第i个位置插入值为e的结点
    	p = L;
    	j = 0;
    	while(p && j < i - 1) {
    		p = p -> next;
    		++j;
    	}
    	if(!p || j > i - 1) return ERROR;
    	s = new LNode;
    	s -> data = e;
    	s -> next = p -> next;
    	p -> next = s;
    	return OK;
    }
    

    注意指针域赋值的顺序。

    要插入到第i个位置,要找到第i - 1个位置。

    时间复杂度是O(n),因为要先遍历找到插入位置

  5. 删除

    和插入一样,需要先找到待删除元素的前驱结点。

    然后修改指针域,并释放待删结点的空间。

    Status ListDelete(LinkList &L, int i) {
    	p = L;
    	j = 0;
    	while(p -> next && j < i - 1) {
    		p = p -> next;
    		++j;
    	}
    	if(!p || j > i - 1) return ERROR;
    	q = p -> next; // 储存被删结点地址 以备释放
    	p -> next = q -> next;
    	delete q;
    	return OK;
    }
    

    时间复杂度是O(n)。

  6. 创建

    (1) 前插法

    ​ 每次生成的新结点插入头结点之后(也就是作为新的首元结点)。

    void CreateList_H(LinkList &L, int n) {
    	L = new LNode;
    	L -> next = NULL;
    	for(int i = 0; i < n; i++) {
    		 p = new LNode;
    		 cin >> p -> data;
    		 p -> next = L -> next;
    		 L -> next = p; // 注意顺序
    	}
    }
    

    ​ 时间复杂度是O(n)。

    (2) 后插法

    ​ 为了便于每次将新结点插入表尾, 需要一个尾指针指向尾结点。

    void CreateList_R(LinkList &L, int n) {
    	L = new LNode;
    	L -> next = NULL;
    	r = L;
    	for(int i = 0; i < n; i++) {
    		p = new LNode;
    		cin >> p -> data;
    		p -> next = NULL;
    		r -> next = p;
    		r = p;
    	}
    }
    

    ​ 时间复杂度是O(n)。

2.5.3 循环链表

最后一个结点的指针域指向头结点

终止条件:p != L && p -> next != L

循环链表的合并:设立尾指针。将第一个表的尾指针指向第二个表的第一个结点,第二个表的尾指针指向第一个表的头结点,然后释放第二个表的头结点。时间复杂度是O(1)

2.5.4 双向链表

克服了单链表单向性的缺点,即既可以查找直接后继,也可以查找直接前驱

一个双链表结点有两个指针域。

d -> next -> prior = d -> prior -> next = d

插入

在第i个位置之前插入元素e

先找到第i - 1个结点的指针域,生成新结点使之数据域赋为e,然后对第i - 1个和第i个结点的指针域赋值(注意赋值顺序),实现新元素的插入。

s = new DulNode;
s -> data = e;
s -> prior = p -> prior;
p -> prior -> next = s;
s -> next = p;
p -> prior = s;

> 第3 4和5 6行代码可以换顺序 但是不能把5 6放在3 4前面 因为5 6改变了p的前驱

删除

删除第i个结点

找到第i - 1个结点的指针域,分别修改被删结点的前驱结点的后继指针和其后继结点的前驱指针

p -> prior -> next = p -> next;
p -> next -> prior = p -> prior;
delete p;

2.6 顺序表和链表的比较

2.6.1 空间

顺序表存储空间预先分配,不能改变;链表空间在使用时才生成,可以改变。

顺序表存储密度大。

2.6.2 时间

顺序表:随机存取 取值的时间复杂度为O(1) 插入/删除时需要大规模进行元素的位置移动

链表:顺序存取 插入删除操作本身的时间复杂度为O(1),但是需要遍历得到要插入/删除的位置

2.7 线性表的应用

2.7.1 线性表合并

2.7.2 有序表合并

  1. 顺序有序表

    void MergeList_Sq(SqList LA, SqList LB, SqList &LC) {
    	LC.length = LA.length + LB.length;
    	LC.elem = new ElemType[LC.length]; // 给新表分配数组空间
    	pc = LC.elem;
    	pa = LA.elem;
    	pb = LB.elem;
    	pa_last = LA.elem + LA.length - 1;
    	pb_last = LB.elem + LB.length - 1;
    	while(pa <= pa_last && pb <= pb_last) { // 两表都未到达表尾
    		if(*pa < *pb) *pc++ = *pa++;
    		else *pc++ = *pb++;
    	}
    	while(pa <= pa_last) *pc++ = *pa++; // LB到达表尾
    	while(pb <= pb_last) *pc++ = *pb++; // LA到达表尾
    }
    

    时间复杂度为O(m + n), 空间复杂度为O(m + n) 【m, n分别为两表的长度】

  2. 链表有序表

    非递减链表A,B,合成一个非递减链表C

    void MergeList(LinkList &LA, LinkList &LB, LinkList &LC) {
    	pa = LA -> next;
    	pb = LB -> next;
    	LC = LA; // LA的头结点作为LC的头结点
    	pc = LC;
    	while(pa && pb) {
    		if(pa -> data <= pb -> data) {
    			pc -> next = pa; // pa指的结点连到pc后面
    			pc = pa;
    			pa = pa -> next;
    		}
    		else {
    			pc -> next = pb;
    			pc = pb;
    			pb = pb -> next;
    		}
    	}
    		pc -> next = pa ? pa : pb; // 非空表的剩余部分链接到pc所指结点之后
    		delete LB; // 释放LB头结点
    }
    

    时间复杂度为O(m + n), 空间复杂度为O(1) 【因为不需要建立新的结点空间,只需要将原表结点间的连接关系解除再重新建立新的关系】

广义表的表示方法

一、头尾链表存储

广义表结点的分类

  1. 表结点:标志域、指示表头的指针域和指示表尾的指针域

  2. 原子结点:标志域和值域

一对确定的表头和表尾可唯一确定列表

typedef enum{ATOM, LIST} ElemTag;
typedef struct GLNode {
    ElemTag tag;
    union {
        AtomType atom; // 原子结点的值域
        struct {struct GLNode *hp, *tp;} ptr; // 表结点的指针域
    };
} *GList;

二、扩展线性链表存储

与上述方法的不同点:原子结点也有三个域。

typedef enum{ATOM, LIST} ElemTag;
typedef struct GLNode {
    ElemTag tag;
    union { // 两种结点的联合部分
        AtomType atom; // 原子结点的值域
        struct GLNode *hp; // 表结点的表头指针
    };
    struct GLNode *tp; /// 指向下一个结点的指针
} *GList;

注意:广义表E(a, E)的深度是无穷。因为它自己是自己的一个元素。

posted on 2023-10-16 12:43  ww0809  阅读(75)  评论(0)    收藏  举报