【数据结构】五分钟自测主干知识(三)

        如前所述,顺序表的插入和删除平均都要移动约一半的元素。当顺序表的长度非常大时,这个时间的消耗还是不容忽视的,因此顺序表常常用于那些无需频繁插入和删除元素的应用。那么对于需要频繁插入和删除的线性表采用什么样的存储方式呢?本节将介绍另一种存储表示方法——链式存储方法

        学好此节,链表基本操作就Over了


2.3线性表的链式表示

单链表

首先介绍单链表

不同于顺序表,链表无需连续的存储单元来存储数据元素,因此每个存储单元仅仅存储数据元素本身之外,还需要存储一个指示其后继元素的信息——指针。

这两部分信息构成一个结点

结点中表示数据元素的域称为数据域,指向后继元素存储位置的域称为指针域

单链表只包含一个指针域,存储结构表示如下:

typedef int ElemType;
typedef struct LNode{
    ElemType data;
    struct LNode* next;
}LNode, * LinkList;//LinkList就可以用来表示一个单链表

网上应该链表操作讲解千千万,不如直接摆个好图,原理知道一通百通(当然需要自我思考)

有关操作顺序只有一个需要注意的:见后面【4.链表插入元素


1.初始化链表

void InitList(LinkList& L) {//初始化链表
	L = NULL;
}

都不需要像数组一样分配空间,创建非常简单


2.求链表长度

int ListLength(LinkList L) {
	LNode* p = new LNode;//C++内语言
	p = L;int k = 0;
	while (p)
	{
		k++; p = p->next;//遍历操作
	}
	return k;
}

可见,我们求链表长度比数组复杂了一些,顺序化也是数组的一大优势,便于查找

很明显当n为结点数时,算法复杂度为O\left ( n \right )(算法复杂度概念见第一讲)


3.链表查找元素

LNode* LocateItem(LinkList L, ElemType e) {
	LNode* p = new LNode;
	p = L;
	while ( p && p->data != e)p = p->next;
	return p;
}

都需要遍历链表,算法复杂度O\left ( n \right )


4.链表插入元素

通过指针链接来体现元素的逻辑关系,不依赖存储区域的连续性

位序的概念在链表中很不方便使用,因此我们插入时不再以位序作为参数,而是直接以某个结点的指针作为参数,将结点插在它的前面(前插)或后面(后插)。

对于顺序表,插在位序为i的元素前面和后面对于顺序表而言操作几乎是同样的;

而对于链表,则差别非常大,请看如下

后插很简单,示意图:

        后插操作主要包括两个指针操作,这两个操作次序是不能颠倒的,否则在p->next被赋值为s后,p原来的后继结点将无法再被访问(可以理解为“失联”)。

s->next=p->next;    //s的后继置为p的原后继
p->next=s;          //p的后继置为s

再来看前插,它多一步定位操作

关键代码步:

q->next=s;    //p的原前驱的后继置为s
s->next=p;    //s的后继置为p

我们之间上整体,看看前插多了什么

void ListInsert(LinkList& L, LNode* p, LNode* s) {
	if (p == L) {
		s->next = L;
		L = s;
	}
	else {
		LNode* q = new LNode;
		q = L;
		while (q && q->next != p)q = q->next;//在这里!这一步定位
		if (q) {
			q->next = s;
			s->next = q;
		}
		else {
			ErrorMsg("p不是L中的结点");
		}
	}
}

这里的ErrorMsg在第二讲中介绍了这个方法,是一个错误处理方法,具体可上翻参照

后插比较简单,和表长度无关,因此其时间复杂度达到了无可比拟的O\left ( 1 \right )

而前插多了一步查找遍历,其时间复杂度为O\left ( n \right )

其实前插当然可以降低时间复杂度到O\left ( 1 \right ),大家可以先想想(怎样少去这个遍历?)

利用后插

s->next=p->next;   //s的后继置为p的原后继
p->next=s;         //p的后继置为s
temp=s->data;      //交换s和p的数据域
s->data=p->data;
p->data=temp;

首先把s插在p后面(后插),然后交换s和p的值,就是相当于p插在s后面,即实现s的前插


5.链表删除结点

void ListDelete(LinkList& L, LNode* p, ElemType& e) {
	if (p == L) { L = p->next; }
	else {
		LNode* q = new LNode;
		q = L;
		while (q && q->next != p)q = q->next;
		if (q)q->next = p->next;
		else ErrorMsg("p不是L中的结点");
	}
	e = p->data; delete p;//保存被删除的元素值,释放结点空间
}

看图说话,时间复杂度为O(n)


2.4线性表的深入探讨

循环链表

循环链表是单链表的一种变化形式,把单链表的最后一个结点的 next 指针指向第一个结点,整个链表就成了一个环。这样在环上任意一个结点出发顺着 next 指针都可以遍历整个链表。一般循序链表都会设置一个头结点,并把头指针指向最后一个结点,这样可以做到首尾兼顾,在头部和尾部插入结点都较容易。


循环链表和单链表的方法基本一致,主要是判断链表遍历结束的方法不一样。在单链表中使用 p==NULL来判断,而在循环链表中则通过p==head->next 来判断。

        循环链表相比于单链表的优点主要体现在表尾插入结点很方便。单链表中要在表尾插入结点必须从头指针开始找到指向最后一个结点的指针,因此时间复杂度为O\left ( n \right );而在循环链表中因为头指针指向最后一个结点,因此可以直接插在头指针所指结点的后面,其时间复杂度为O\left ( 1 \right )


双向链表

既然我们可以为每个结点设立一个指向后继的指针,当然也可以设立一个指向前驱的指针,这样就可以直接访问了。这种结构的链表称为双向链表(Double Linked List)

注意:后继指针代表逻辑关系,前驱指针不代表逻辑关系。

typedef int ElemType;
typedef struct DLNode {
	ElemType data;
	struct DLNode* prior;//多了一个指针域
	struct DLNode* next;
}DLNode,*DLinkList;

双向链表一般也采用带头结点的循环链表,并且有一个头指针指向头结点,所以称为双向循环链表。空的双向循环链表只有一个头结点,并且前驱和后继的指针都指向自身。如下图

双向链表的结构极大地方便了链表的前插和删除功能,避免了从头结点开始的查找前驱操作,使原来链表中前插和删除操作的时间复杂度均达到O\left ( 1 \right )

双向链表中结点插入操作的实现如下:

void ListInsert(DLinkList& L, DLNode* p, DLNode* s) {
	//在双向循环链表L中结点p前插入结点s
	s->prior = p->prior;
	s->next = p;
	p->prior->next = s;
	p->prior = s;//后面两个指针赋值次序不能颠倒!
}

双向链表中结点的删除操作的实现如下:

void ListDelete(DLinkList& L, DLNode* p, ElemType& e) {
	//将双向循环链表L中p结点删除,并把元素值赋予e
	e = p->data;
	p->prior->next = p->next;
	p->next->prior = p->prior;
	delete p;
}

针对链表不易定位到尾结点,我们使用了循环链表

针对链表插入难找到前驱,我们使用了双向链表

而针对链表难求长度,我们又可以使用什么呢?

我们可以定义一个这样的高级链表类型:

typedef struct {//高级链表类型
	LinkList head, tail;
	int length;
}AdvLinkList;

其中LinkList类型在前面已申明

直接使其中一个属性成员为表长,甚至包括尾指针。

一个例子:

把数组A[n]所表示的顺序表的元素按序创建AdvLinkList。

void CreateList(AdvLinkList& L, ElemType A[], int n) {
	//把数组A[n]的元素按序存储在AdvLinkList中
	L.head = L.tail = new LNode;
	L.head->next = NULL;
	L.length = 0;
	for (int i = 0; i < n - 1; i++) {
		LNode* s = new LNode;
		s->data = A[i];
		s->next = NULL;
		L.tail->next = s;
		L.tail = s;
		L.length++;
	}
}

有序表

线性表中元素之间的逻辑关系是序偶关系。对元素值并没有任何约束。

若在元素上加以约束,如按照值的大小来依次排列元素,那么很多算法就会得到简化

如果一个线性表中元素之间可比较大小,并且对于所有元素都按照非递增或者非递减有序排列,即a_i\leq a_{i+1}a_i\geq a_{i+1}(i=1,2,\dots,n-1),那么称该线性表为有序表

有序表既可以是顺序存储的,也可以是链式存储的。

所以不能随便插入了,得遍历查看大小后后插

时间复杂度和顺序表的前插操作一样为O\left ( n \right )

【eg】将非纯集合L纯化(没有相同元素):

之前顺序表或链表来解决本例算法复杂度都是O\left ( n^2 \right )

现在是只需要扫描一遍L,时间复杂度是O\left ( n \right )


终于讲完啦,肝完这节开咕~

有什么有关数据结构的问题可以评论区讨论一下,全天在线(^.^)Y Ya!!

下一讲是数据结构经典,典中之典,栈!

附链接:【数据结构】五分钟自测主干知识(四)

http://t.csdnimg.cn/vi8E6icon-default.png?t=N7T8http://t.csdnimg.cn/vi8E6

  • 35
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值