《数据结构》学习笔记01:线性表

         本文旨在讲解基本的数据结构知识(主要是线性表),使用C/C++语言从零实现各种数据结构,适合于代码能力薄弱的初学者以及想要复习数据结构的读者们。笔者能力有限,所讲述的内容也无法面面俱到,如有表述错误,欢迎指正!

一、线性表

1、逻辑结构与物理结构

        学习每一种数据结构,一定不能将“逻辑结构”与“物理结构”这两个概念抛弃,接下来对这两个概念进行阐述:

(1)所谓“逻辑结构”,便是数据与数据之间的关系,主要有:一对一(线性结构)、一对多(树形结构),多对多(图形结构)和集合结构(无序),而我们更关心的是前三种逻辑结构。

(2)所谓“物理结构”,便是数据结构的具体实现方式,主要有:顺序结构、链式结构、索引结构和哈希结构(哈希表)。

        不管是哪一种“逻辑结构”,其基本实现方式都离不开顺序结构和链式结构,也就是数组和链表,包括索引结构和哈希结构的底层实现也是数组和链表,所以学好数组和链表是学好数据结构的基本要求。

2、线性表的基本概念

        线性表是最基本的符合“线性结构”的数据结构,数据与数据之间有前后顺序之分(有序),一般将线性表中元素的个数n定义为线性表的长度,当n=0时为空表。

        针对于非空的线性表,有以下特点:

(1)存在唯一的一个“第一个”元素和一个“最后一个”元素。

(2)除第一个元素外,其他元素都有且仅有一个前驱(前一个元素)。

(3)除最后一个元素外,其他元素都有且仅有一个后继(后一个元素)。

3、线性表的实现

        线性表的逻辑结构为线性结构,物理结构有顺序存储方式(数组实现)和链式存储方式(链表实现)。

二、顺序表

1、基本概念

        顺序表是指使用顺序存储结构的线性表,使用一组地址连续的存储单元依次存储各个元素。对于顺序表,逻辑上相邻的元素在物理位置上也是相邻的。

        假设每个元素占用L个存储单元,则对于第i个元素a[i]的存储位置有以下关系:

LOC(a_{i})=LOC(a_{1})+(i-1)*L

        根据以上公式可以得出,若要确定顺序表中的某一个元素a的地址,只需要知道首元素的地址与该元素a的次序(索引)即可通过运算获得。所以对于顺序表,其中的任意一个数据元素都可以随机存取(按下标存取)。

2、结构定义

        基于上述的公式与顺序表的特点,可以使用数组实现顺序表,以下是顺序表的结构定义:

const int MAXSIZE = 100;  //顺序表最大容量
typedef int ElemType;     //顺序表元素的数据类型

/* 顺序表 */
typedef struct
{
	ElemType* elem;       //元素数组首地址
	int length;           //顺序表长度
}SqList;

3、初始化

        顺序表的初始化操作主要是针对结构定义中的两个结构变量初始化,对于指针elem可以使用C++的new关键字进行初始化,长度length则初始化为0,以下是初始化代码:

void InitList(SqList& L)
{
	L.elem = new ElemType[MAXSIZE];
	if (!L.elem)
		return;
	L.length = 0;
}

4、增删改查

(1)查:

        如果非要问增删改查四个操作中哪个操作最重要,那笔者认为应该是查操作,因为在进行增删改操作前,必须要先查找到对应的元素。

        查找有两种类型:按值查找(找到对应的值,存在返回位置)、按位查找(找到对应位置的元素,存在返回元素的值)。对于顺序表而言,按位查找只需要输入索引(位置,从1开始)即可,而按值查找则需要从头遍历查找(如果是有序数组可以采用二分查找),以下是实现代码:

//按位查找
ElemType GetElem(const SqList& L, int index) 
{
	if (index < 1 || index > L.length)
		return -1;
	return L.elem[index - 1];
}

//按值查找
int LocateElem(const SqList& L, ElemType e) 
{ 
	for (int i = 0; i < L.length; i++)
		if (L.elem[i] == e)
			return i + 1;
	return -1;
}

(2)改:

        改是在查的基础上进行的,同样有按位修改和按值修改,不过按值修改可以转换为按位修改(先使用按值查找找到对应位置,再进行按位修改),后续只讲述按位的增删改(按值的增删改可转换为按位增删改),接下来是按位修改的代码:

void setElem(SqList& L, int index, ElemType e) 
{
	if (index < 1 || index > L.length || L.length == MAXSIZE)
		return;
	L.elem[index - 1] = e;
}

(3)增:

        顺序表的增操作需要对应的移动元素,一般而言,假设当前表长度为n,若想在第i个位置插入一个元素,则需要将原本第i个到第n个元素均向后移动一个单位(从第n个开始移动,一直到第i个移动完毕),之后将需要插入的元素放入空出来的第i个位置。

        注意!在插入数据前需判断表长度是否达到最大容量,若达到则不允许插入,否则会造成溢出错误。

void ListInsert(SqList& L, int index, ElemType e)
{
	if (index < 1 || index > L.length + 1  || L.length == 0)
		return;
	for (int i = L.length - 1; i >= index - 1; i--)
		L.elem[i + 1] = L.elem[i];
	L.elem[index - 1] = e;
	++L.length;            //表长度加1
}

 (4)删:

        顺序表的删操作同样需要移动元素,一般而言,假设当前表长度为n,若想删除第i个位置的元素,则需要将原本第i+1个到第n个元素均向前移动一个单位(从第i+1个开始移动,一直到第n个移动完毕),之后将原本第n个位置的元素释放。

        注意!在删除数据前需判断表是否为空,若为空不允许删除。

int ListDelete(SqList& L, int index)
{
	if (index<1 || index>L.length)
		return -1;
	int e = L.elem[index - 1];
	for (int i = index; i < L.length; i++)
		L.elem[i - 1] = L.elem[i];
	--L.length;              //表长度减1
	return e;                //返回删除元素的值
}    

5、遍历

        所谓遍历,即是对表中所有元素均访问一遍。对于顺序表而言有正序遍历与逆序遍历两种方式,以下选择的是正序遍历,并在遍历的过程中打印数据。

void printList(const SqList& L)
{
	if (!L.elem || L.length == 0)
		return;
	for (int i = 0; i < L.length; i++)
		cout << L.elem[i] << " ";
	cout << endl;
}

6、应用

(1)逆转顺序表:已知顺序表LA,将该表逆转(逆序),且要求空间复杂度为O(1)。

解题思路:

        本题要求是构造一个与原来的表LA逆序的表,且要求空间复杂度为O(1),即要求不使用另外的存储空间,若LA的长度为n,则所能使用的空间即为n。为此可以采用先删除尾元素(最后一个元素),再将该元素插入头元素(第一个元素)的方式,这样的一组操作使得表长始终为n,而删除与插入操作使用上述的顺序表增删操作即可。之后便是循环以上操作,直到所有元素均逆转完毕。实现代码如下:

//逆转顺序表
void RotateList_Sq(SqList& L)
{
	if (Empty(L))
		return;
	int e, n = Length(L);
	for (int i = 0; i < n - 1; i++)
	{
		e = ListDelete(L, n);
		ListInsert(L, i + 1, e);
	}
}

(2)合并有序顺序表:已知两个有序顺序表LA和LB,将两个顺序表合并为有序的LC。

解题思路:

        首先初始化LC,即设定LC的长度为LA和LB长度之和,并使用new操作初始化LC的元素指针,并定义一个指针pc指向LC的首元素。之后设定4个指针,分别为pa、pa_last、pb、pb_last,分别指向LA的首元素与尾元素、LB的首元素与尾元素。

        之后比较pa与pb指向的元素的大小,小的一方(例如pa)赋值给pc,并更新pc和小的一方的指针(pa),循环此操作,直到pa==pa_last(表示pa移到到LA的终点)或pb==pb_last(表示pb移动到LB的终点)。

        最后将未完全遍历完的表的剩余部分接到LC最后(例如此时pb!=pb_last,则表示LB未完全遍历完,将pb后续的部分接到LC最后)。

        实现代码如下:

//合并有序顺序表
void MergeList_Sq(SqList LA, SqList LB, SqList& LC)
{
	LC.length = LA.length + LB.length;
	LC.elem = new int[LC.length];
	int* pc = LC.elem;

	int* pa = LA.elem,
		* pa_last = LA.elem + LA.length - 1;
	int* pb = LB.elem,
		* pb_last = LB.elem + LB.length - 1;

	while ((pa <= pa_last) && (pb <= pb_last))
	{
		if (*pa <= *pb)      //判断pa和pb指向的元素哪个小,小者赋值给pc
			*pc++ = *pa++;
		else
			*pc++ = *pb++;
	}
	while (pa <= pa_last)    //LA未遍历完,将剩余部分接到LC最后
		*pc++ = *pa++;
	while (pb <= pb_last)    //LB未遍历完,将剩余部分接到LC最后
		*pc++ = *pb++;
}

三、单链表

1、基本概念

        链表是指使用链式存储结构的线性表,主要有单向链表(也叫单链表)、双向链表、循环链表等(本文主要介绍单链表),链表不要求逻辑上相邻的元素在物理存储上也相邻,相邻的元素之间使用指针链接。

        链表的核心结构是链表结点LNode,对于每一个链表结点,包括数据域data,与指针域next,数据域表示当前结点存储的数据,而指针域表示指向下一个结点的指针。(对于双向链表的指针域有两个指针,一个指向后继结点next,一个指向前驱结点prior)。

2、结构定义

        笔者这里对数据的类型采用了int型,读者可根据自己需要修改数据的类型。以下是对单链表结构定义的样例代码:

/* 链表 */
typedef struct LNode
{
	int data;
	struct LNode* next;
}LNode, * LinkList;

3、初始化

        一般使用指向链表的头结点的指针L表示链表,链表的初始化主要是对L和L指向的头结点初始化,对L使用new关键字初始化,并初始化L->next为NULL,表示当前链表为空(头结点不计入链表长度计算)。实现代码如下:

void InitList(LinkList& L)
{
	L = new LNode;
	if (!L)
		return;
	L->data = 0;
	L->next = NULL;
}

4、增删改查

(1)查:

        对单链表的查找包括按值查找和按位查找,但两种方式均需要从头开始遍历,只不过是循环的判断条件不同,以下是实现的代码:

//按位查找
int GetElem(const LinkList& L, int index) 
{
	LNode* p = L->next;
	int i = 1;
	while (p && i < index)
	{
		p = p->next;
		++i;
	}
	if (!p || i > index)
		return -1;
	return p->data;
}

//按值查找
LNode* LocateElem(const LinkList& L,int e)
{
	LNode* p = L->next;
	while (p && p->data != e)
		p = p->next;
	return p;
}

(2)改:

        按位修改的实现与按位查找的实现类似,只不过将返回数据变为修改数据。

void setElem(LinkList& L, int index, int e)
{
	LNode* p = L->next;
	int i = 1;
	while (p && i < index)
	{
		p = p->next;
		++i;
	}
	if (!p || i > index)
		return;
	p->data = e;
}

(3)增:

void ListInsert(LinkList& L, int index, int e) //index从0开始
{
	LNode* p = L;
	int i = 0;
	while (p && i < index - 1)
	{
		p = p->next;
		++i;
	}
	if (!p || i > index - 1)
		return;
	LNode* s = new LNode;
	s->data = e;
	s->next = p->next;
	p->next = s;
	++(L->data);
}

(4)删:

LNode* ListDelete(LinkList& L, int index) 
{
	LNode* p = L;
	int i = 0;
	while (p->next && i < index - 1)
	{
		p = p->next;
		++i;
	}
	if (!(p->next) || i > index - 1)
		return NULL;
	LNode* q = p->next;
	p->next = q->next;
	--(L->data);
	return q;
}

5、单链表的创建

        单链表的创建方式有头插法(新结点插在链表头部)和尾插法(新结点插在链表尾部)。

(1)对于头插法:

        首先初始化头指针L(L->next赋值NULL),定义指针p指向新插入的结点,并对p指向的结点的数据域进行赋值操作(需要插入的数据)。

        之后令p->next=L->next(若此时链表为空,则新结点p成为第一个结点,若链表非空,则新结点p的下一个结点为原本链表的第一个结点)。

        最后更新L->next,令其指向链表新的首结点p,即令L->next=p。

void CreateList_H(LinkList& L, int nums[], int len) //nums数组为待插入的数据
{
	L = new LNode;
	L->next = NULL;
	for (int i = 0; i < len; i++) 
	{
		LNode* p = new LNode;
		p->data = nums[i];

		p->next = L->next;
		L->next = p;
	}
}

(2)对于尾插法:

        首先初始化头指针L,同时定义尾指针r(始终指向链表最后一个结点),并初始化r=L,即初始状态下,首指针和尾指针均指向头结点。

        之后定义指向新结点的指针p,并对p的数据域赋值,令p->next=NULL(p指向的结点为链表最后一个结点,故其下一个结点为空),令尾结点的下一个结点为结点p,即r->next= p。

        最后更新尾指针r,令r指向新结点p,即r=p。

void CreateList_R(LinkList& L, int nums[], int len) 
{
	L = new LNode;
	L->next = NULL;
	LNode* r = L;
	for (int i = 0; i < len; i++) 
	{
		LNode* p = new LNode;
		p->data = nums[i];

		p->next = NULL;
		r->next = p;
		r = p;
	}
}

6、遍历

        对单链表的遍历只能从头开始,不能像顺序表那样正序和逆序遍历。以下是链表遍历代码:

void printList(const LinkList& L)
{
	LNode* p = L->next;
	while (p)
	{
		cout << p->data << " ";
		p = p->next;
	}
	cout << endl;
}

7、应用

(1)逆转链表:逆转一个链表L,要求空间复杂度为O(1)。

解题思路:

        解决该问题采用每次均删除第一个结点再使用头插法构造新链表(头插法构造的链表顺序是插入顺序的逆序),具体代码如下:

//逆转链表
void RotateList_L(LinkList& L)
{
	LNode* p = L->next;
	L->next = NULL;
	while (p) 
	{
		LNode* q = p->next;
		p->next = L->next;
		L->next = p;
		p = q;
	}
}

(2)合并有序链表:将两个有序链表LA和LB合并为新的有序链表LC,要求空间复杂度为O(1)。

解题思路:

        首先定义两个指针pa和pb分别指向LA和LB的第一个结点(头结点的下一个结点,头结点不计入链表长度计算),令LC=LA(不使用另外的存储空间),定义指针pc=LC。

        之后比较pa的数据和pb的数据,若pa指向的数据小,则令pc的下一个结点为pa指向的结点(pc->next=pa),并更新pc和pa(pc=pa,pa=pa-next),pb指向数据小的情况相同。循环以上操作,直到pa或pb有一个为NULL(有一个链表遍历完毕)。

        最后将未遍历完的链表剩余的部分接到新链表最后。

//合并有序链表
void MergeList_L(LinkList& LA, LinkList& LB, LinkList& LC)
{
	LNode* pa = LA->next;
	LNode* pb = LB->next;
	LC = LA;
	LNode* pc = LC;
	while (pa && pb)
	{
		if (pa->data <= pb->data)
		{
			pc->next = pa;
			pc = pa;
			pa = pa->next;
		}
		else
		{
			pc->next = pb;
			pc = pb;
			pb = pb->next;
		}
	}
	pc->next = pa ? pa : pb;
	delete LB;
}
  • 34
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值