Bootstrap

数据结构:线性表(详解)

线性表

线性表的知识框架:

线性表的定义:

线性表是具有相同数据类型的n(n > 0)个数据元素的有限序列,当n = 0时线性表为一个空表。
若用L命名为线性表,则
数据集合为L= {a1,a2,…,an},其中a1称为表头元素,an称为表尾元素。除第一个元素外,每个元素有且仅有一个直接前驱。除最后一个元素外,每个元素有且仅有一个直接后继。

线性表的特点:

1.表中元素的个数有限
2.表中元素具有逻辑上的顺序表,表中元素有其先后次序
3.表中元素都是数据元素,每个元素都是单个元素
4.表中元素的数据类型相同,所以每个元素占有相同大小的存储空间
5.线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构

线性表的顺序存储结构

顺序表的的定义:

线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素。

顺序表的特点:

1.顺序表的存储密度高,每个结点只存储数据结构。
2.顺序表逻辑上相邻的元素在物理上也相邻,所以插入和删除操作需要移动大量数据
3.顺序表是随机访问,即通过首地址和元素序号可在时间O(1)内找到指定的元素

顺序表代码实现:

    顺序表储存结构:

typedef int SLTDataType;
typedef struct SeqList
{
	SLDatatype capacity;//数组的容量
	SLDatatype size;//有效数据
	SLDatatype* arr;
}SL;
顺序表所有接口:
//初始化/销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);

//顺序表的头部/尾部插入
void SLPushBack(SL* ps, SLDatatype x);
void SLPushFront(SL* ps, SLDatatype x);

//顺序表的头部/尾部删除
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);

//指定位置之前插入数据/删除指定位置数据
void SLInsert(SL* ps, SLDatatype pos, SLDatatype x);
void SLErase(SL* ps, SLDatatype pos);
顺序表初始化和销毁:
void SLInit(SL* ps)
{
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}

顺序表输出:

void SLPrint(SL* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}
    printf("\n");
}
顺序表扩容:
void SLIncreaseCapacity(SL* ps)
{
	if (ps->capacity == ps->size)//当有效数据与实际数据相等则需要扩容
	{
		SLDatatype NewCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, NewCapacity * sizeof(int));
		if (tmp == NULL)
		{
			perror("malloc error");
			exit(-1);
		}
		ps->arr = tmp; //仅当 SL 结构被销毁时,用于 ps->arr 的内存才会被释放。
		ps->capacity = NewCapacity;
	}
}
顺序表尾插:
void SLPushBack(SL* ps, SLDatatype x)
{
	assert(ps);
SLIncreaseCapacity(ps);//判断是否需要扩容
ps->arr[ps->size++] = x;
}

顺序表尾插输出:

顺序表头插:
void SLPushFront(SL* ps, SLDatatype x)
{
	assert(ps);
SLIncreaseCapacity(ps);//判断是否需要扩容
for (int i = ps->size; i > 0; i--)
{
	ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}

顺序表头插输出:

顺序表尾删:
void SLPopBack(SL* ps)
{
	assert(ps);
	assert(ps->size);//判断数组是否为空
	ps->size--;//size--只是在逻辑层面删除了而已,但物理层面还是实际存在的 
               //只是我们不将它看成 顺序表的一部分,所以就可以理解为删除了         
}

顺序表尾删输出:

顺序表头删:
void SLPopFront(SL* ps)
{
	assert(ps);
	assert(ps->size);
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

顺序表头删输出:

在指定位置插入:
void SLInsert(SL* ps, SLDatatype pos, SLDatatype x)
{
	assert(ps);
	SLCheckCapacity(ps);
	assert(pos >= 0 && pos < ps->size);//pos如果小于0那就没什么意义并且不能等于ps->size如果等于那就相当于尾插了
	for (int i = ps->size; i > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

指定位置输出:

在指定位置删除:
void SLErase(SL* ps, SLDatatype pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

指定位置输出:

顺序表的优缺点:

  优点:1.空间利用率高

             2.插入删除方便

             3.结构简单

  缺点:1.访问效率低

            2.插入和删除操作可能导致元素移动

            3.难以实现部分元素的随机访问

线性表的链式存储结构:

单链表的定义:

单链表(SLL)是一种由节点序列组成的链接数据结构,每个节点包含一个数据元素和一个指向下一个节点的指针。单链表以其简单性和效率而闻名,广泛应用于各种应用程序。                                                                                  单链表结构如下:

单链表代码实现:

单链表存储结构:
typedef int SLTDataType;
typedef struct SList
{
	struct SList* next;//指针域
	SLTDataType data;//数据域
}SL;
单链表所有接口:

//链表的输出、销毁
void SLPrint(SL* phead);
void SListDesTroy(SL** pphead);

//链表的尾插、头插
void SLPushBack(SL** pphead, SLTDataType x);
void SLPushFront(SL** pphead, SLTDataType x);

//链表的尾删、头删
void SLPopBack(SL** pphead);
void SLPopFront(SL** pphead);

//查找
SL* SLTFind(SL* phead, SLTDataType x);

//在指定位置之前插入数据、之后插入数据
void SLTInsert(SL** pphead, SL* pos, SLTDataType x);
void SLTInsertAfter(SL* pos, SLTDataType x);

//删除pos节点、pos之后的节点
void SLTErase(SL** pphead, SL* pos);
void SLTEraseAfter(SL* pos);
单链表输出:

void SLPrint(SL* phead)
{
	SL* pur = phead;
	while (pur)
	{
		printf("%d->", pur->data);
		pur = pur->next;
	}
	printf("NULL");
	printf("\n");
}
单链表销毁:

void SListDesTroy(SL** pphead)
{
	void SListDesTroy(SL** pphead)
{
	assert(pphead && *pphead);
	SL* Pfast = (*pphead)->next;
	SL* Pslow = *pphead;
	while (Pslow && Pfast != NULL)
	{
		free(Pslow);
		Pslow = NULL;

		Pslow = Pfast;
		Pfast = Pfast->next;
	}
    //销毁尾节点
	free(Pslow);
	Pslow = NULL;
    //free完之后再将头节点置为空
	*pphead = NULL;
}
}

销毁链表输出:

扩容单链表空间:

SL* SLTBuyNode(SLTDataType x)
{
	SL* Newnode = (SL*)malloc(sizeof(SL));
	SL*	ptmp = Newnode;
	if (ptmp == NULL)
	{
		perror("malloc error");
		exit(-1);
	}
	Newnode->data = x;
	Newnode->next = NULL;
	return Newnode;
}
单链表尾插:

//为什么需要二级指针而不用一级指针呢,因为传一级的话改变不了它的指向而二级指针可以
//就相当于C语言里面传值和传地址一个道理
void SLPushBack(SL** pphead, SLTDataType x)
{
	//pphead:相当于指针指向节点的那个指针的地址
    //*pphead:相当于节点地址
    //**pphead:相当于节点里的数据
	assert(pphead);//判断pphead指针是否为空和节点是否为空
	//assert(*pphead);//判断pphead节点是否为空
	SL* newnode = SLTBuyNode(x);
	if (*pphead == NULL)//当链表为空时那么newnode就可以作为链表的新节点
	{
		*pphead = newnode;
		return;
	}
	//链表不为空
	SL* Ptmp = *pphead;
	while (Ptmp->next)
	{
		Ptmp = Ptmp->next;
	}
	Ptmp->next = newnode;
}

单链表尾插输出:

本质上,单指针用于“只读”操作,而双指针用于“读写”操作。

以下是一个简化的类比来说明区别:

想象一个链表就像一列火车车厢。每个车厢代表列表中的一个节点,车厢之间的连接代表指针。

单指针就像火车上的乘客。他们可以从一辆车厢移动到另一辆车厢,观察每辆车厢的内容(数据访问),但他们不能直接拆卸或连接车厢(修改列表结构)。

双指针就像火车乘务员。他们不仅有权在车厢之间移动,还可以解耦和耦合车厢,修改列车的组成(修改列表结构)。
如果还是搞不懂可以点开这个链接,这个博主写的非常好http://t.csdnimg.cn/yNoDo

单链表头插:

void SLPushFront(SL** pphead, SLTDataType x)
{
	assert(pphead);
	SL* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}
	newnode->next = *pphead;
	*pphead = newnode;
}

单链表头插输出:

单链表尾插:


void SLPopBack(SL** pphead)
{
	assert(pphead && *pphead);//判断pphead指针是否为空和节点是否为空
	//只有一个节点
	//因为->符号的优先级比*符号的优先级高所以不加括号就是pphead->next
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//多个节点
	SL* Pfast = *pphead;
	SL* Pslow = NULL;
	//如果是用Pfast去判断的话那等while结束的时候到它会指向NULL,Plast会指向最后一个节点
    //最后我们是把NULL赋给Pfast->next以此来达到尾删的效果,那这样都不是删除最后一个节点而是给NULL赋NULL。
	//所以是要用Pfast->next去进行判断
	while (Pfast->next)
	{
		Pslow = Pfast;
		Pfast = Pfast->next;
	}
	Pslow->next = NULL;
	//最后别忘记销毁尾节点
	free(Pfast);
	Pfast = NULL;
}

单链表尾删输出:

单链表头删:

void SLPopFront(SL** pphead)
{
	assert(pphead && *pphead);
	//只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//多个节点
	SL* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

单链表头删输出:

单链表查找:

SL* SLTFind(SL* phead, SLTDataType x)
{
	assert(phead);
	SL* Pcur = phead;
	while (Pcur)
	{
		if (Pcur->data == x)
		{
			return Pcur;
		}
		Pcur = Pcur->next;
	}
	return NULL;
}

查找输出:

在指定位置之前插入数据:
void SLTInsert(SL** pphead, SL* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);//判断pos指针是否为空
	SL* newnode = SLTBuyNode(x);//扩容一个新节点
	//假设pos为头节点
	if (pos == *pphead)
	{   //头插
		SLPushFront(pphead , x);
		return;//插入完新数据直接return
	}
	SL* Pur = *pphead;
	//不为头节点
	while (Pur)
	{
		if (Pur->next == pos)
		{
			Pur->next = newnode;
			newnode->next = pos;
		}
		Pur = Pur->next;
	}
	return;
}

指定位置之前输出:

在指定位置之后插入数据:

//尾插不需要pos之前的指针,所以pphead在这里不起作用
void SLTInsertAfter(SL* pos, SLTDataType x)
{
	assert(pos);
	SL* newnode = SLTBuyNode(x);
	SL* Ptur = pos->next;//防止找不到它的下一个节点
	pos->next = newnode;
	newnode->next = Ptur;
}

指定位置之后输出:

删除pos指针:
void SLTErase(SL** pphead, SL* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	//pos刚好是头结点,执行头删
	if (*pphead == pos) 
	{
		//头删
		SLPopFront(pphead);
		return;
	}
	SL* Pur = *pphead;
	//不为头节点
	while (Pur->next != pos)
	{
		Pur = Pur->next;
	}
	Pur->next = pos->next;
	free(pos);
	pos = NULL;
	return;
	//为什么错误?上面为什么对?
	//因为上面就是他先把pos置为空但是那个循环不会结束等到Pur->NULL时
	//那这又与pos相等了,如果再去访问pos->next就会报错
	//访问空指针的next就会出问题但是访问有效指针的next且那个next为空这样并不会报错
	/*while (Pur)
	{
		if (Pur->next == pos)
		{
			Pur->next = pos->next;
			free(pos);
			pos = NULL;
		}
		Pur = Pur->next;
	}*/
}

删除pos指针输出:

删除pos之后节点
void SLTEraseAfter(SL* pos)
{
	assert(pos);
	SL* Pur = pos->next;//保存Pur节点等下需要销毁这个节点
	pos->next = pos->next->next;
	free(Pur);
	Pur = NULL;
}

删除pos之后节点输出:

                                     上述代码全部都是用不带头节点编写而成 

单链表的优缺点:

 优点:
             1.简单易懂
             2.插入和删除操作高效
             3.动态内存管理

缺点:
             1.无法随机访问
             2.插入操作会破坏原有顺序
             3.无法向后遍历

双链表的定义:

在计算机科学中,双链表(DLL)是一种由元素序列组成的链接数据结构,每个元素称为节点,每个节点都有两个指针:一个指向序列中的下一个节点,另一个指向前面的节点。每个节点还包含一个数据元素。

双链表代码实现:

双链表存储结构:
typedef int LTDataType;
typedef struct DList
{
	struct DList* next;//后继节点
	struct DList* prev;//前驱节点
	LTDataType data;//节点数据
}DL;
双链表所有接口:
//初始化、销毁、输出
void LTInit(DL** pphead);
void LTDesTroy(DL** pphead);
void LTPrint(DL* phead);

//尾插、头插
void LTPushBack(DL* phead, LTDataType x);
void LTPushFront(DL* phead, LTDataType x);

//头删、尾删
void LTPopBack(DL* phead);
void LTPopFront(DL* phead);

//查找
DL* LTFind(DL* phead, LTDataType x);

//在pos位置之后插入数据、删除pos位置的数据
void LTInsert(DL* pos, LTDataType x);
void LTErase(DL* pos);
双链表初始化:
void LTInit(DL** pphead)
{
	*pphead = (DL*)malloc(sizeof(DL));//为哨兵位开辟空间
	if (*pphead == NULL)
	{
		perror("malloc error");
		exit(-1);
	}
    //双链表节点的初始化是前驱和后继指针都是指向自身
	(*pphead)->next = (*pphead)->prev = *pphead;
	(*pphead)->data = 0;
}

双链表初始化输出:

双链表销毁:
void  LTDesTroy(DL** pphead)
{
	assert(pphead);
	assert(*pphead);
	DL* Pfast = (*pphead)->next->next;
	DL* Pslow = (*pphead)->next;
	while (Pslow != NULL)
	{
		free(Pslow);
		Pslow = NULL;
		Pslow = Pfast;
		Pfast = Pfast->next;
	}
	free(*pphead);
	*pphead = NULL;
}

双链表销毁输出:

扩展双链表空间:
DL* DLTBuyNode(LTDataType x)
{
	DL* NewNode = (DL*)malloc(sizeof(DL));//为新节点开辟空间
	DL* Ptmp = NewNode;
	if (Ptmp == NULL)
	{
		perror("malloc error");
		exit(-1);
	}
	Ptmp->data = x;
	Ptmp->next = Ptmp->prev = NewNode;
	return NewNode;
}
双链表输出:
void LTPrint(DL* phead)
{
	assert(phead);
	DL* Pur = phead->next;//因为哨兵位没有数据所以不需要遍历它
	while (Pur != phead)
	{
		printf("%d->", Pur->data);
		Pur = Pur->next;
	}
	printf("\n");
}
双链表尾插:
void LTPushBack(DL* phead, LTDataType x)
{
    assert(phead);//判断phead指针是否为空
	DL* Newnode = DLTBuyNode(x);
	//先改变新节点
	Newnode->next = phead;//让新节点的后继指针指向头节点(原先是尾节点指向哨兵位的前驱)
	Newnode->prev = phead->prev;//让新节点的前驱指针指向尾节点

	//再改变尾节点
	phead->prev->next = Newnode;//让尾节点的后继指针指向新节点
	phead->prev = Newnode;//哨兵位的前驱指针指向新节点
}

双链表尾插输出:

双链表头插:
//双链表的头插并不是在哨兵位之前插入数据而是第一个有效节点之前插入
//在哨兵位之前插入数据就相当尾插,因为哨兵位前面就是最后一个有效节点
void LTPushFront(DL* phead, LTDataType x)
{
	assert(phead);
	DL* Newnode = DLTBuyNode(x);
	//先改变新节点的指向
	Newnode->next = phead->next;
	Newnode->prev = phead;

	phead->next->prev = Newnode;
	phead->next = Newnode;
}

双链表头插输出:

双链表尾删:
void LTPopBack(DL* phead)
{
	assert(phead);
	assert(phead->next != phead);//哨兵位指向自己就相当于双链表为空
	DL* pre = phead->prev;
	//先让尾节点的前一个节点的next指向哨兵位再让哨兵位的前驱指针指向尾节点的前一个节点那这样下 
    //来尾节点就相当于被销毁了
	phead->prev->prev->next = phead;//phead->prev就相当于尾节点然后phead->prev->prev就相当 
                                    //于尾节点的前一个节点
	phead->prev = phead->prev->prev;
	free(pre);
	pre = NULL;
	
}

双链表尾删输出:

双链表头删:
void LTPopFront(DL* phead)
{
	assert(phead);
	assert(phead->next != phead);
	DL* next = phead->next;//如果不先保存原哨兵位的next地址那删除完节点后再去 
                           //free(phead>next)这样就是删除新头节点了并不是再删除旧的头节点
	phead->next->next->prev = phead;
	phead->next = phead->next->next;
	free(next);
	next = NULL;
}

双链表头删输出:

双链表查找:
DL* LTFind(DL* phead, LTDataType x)
{
	assert(phead);
	DL* Pur = phead->next;//区别于与单链表的是这里传的并不是头节点而是头节点next
	while (Pur != phead)
	{
		if (Pur->data == x)
		{
			return Pur;
		}
		Pur = Pur->next;
	}
	return NULL;
}

双链表查找输出:

在指定位置之后插入数据:
void LTInsert(DL* pos, LTDataType x)
{
	assert(pos);
	DL* Newnode = DLTBuyNode(x);
	//依然是先修改新节点的指向
	Newnode->next = pos->next;
	Newnode->prev = pos;

	//不能先改变pos->next的指向因为修改了就找不到pos后面的节点
	//所以指向先改变pos后面节点的指向然后再改变pos的指向
	pos->next->prev = Newnode;
	pos->next = Newnode;
	
}

在指定位置之后插入数据输出:

删除pos位置的数据:
void LTErase(DL* pos)
{
	assert(pos);
	//先改变pos前面的节点那就找不到pos后面的节点,所以只能先改变后面再改变前面
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
}

删除pos位置的数据输出:

链表与顺序表的比较:

1.结构:

链表: 链表由一系列节点组成,每个节点包含数据元素和指向下一个节点的指针。节点之间的连接是通过指针实现的,因此链表是一种非连续的存储结构

顺序表顺序表将元素存储在连续的内存空间中,每个元素占用一个固定大小的单元。元素之间的访问通过索引实现,因此顺序表是一种连续的存储结构

2.逻辑结构与物理结构:

采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系时通过指针链接来表示的。

3.遍历、插入、删除操作:

插入:在链表中插入新元素,需要修改相邻节点的指针以保持链表的顺序。在顺序表中插入新元素,如果空间不足则需要重新分配内存或移动现有元素。

删除:在链表中删除元素,需要修改相邻节点的指针以跳过被删除的元素。在顺序表中删除元素,需要将被删除元素后面的元素向前移动一个位置。

遍历在链表中遍历元素,需要逐个遍历节点并访问数据元素。在顺序表中遍历元素,可以使用循环访问每个元素

4.应用:

链表: 适用于需要频繁插入和删除操作的情景,因为链表的插入和删除操作效率较高,且不需要预先分配固定数量的内存空间。例如,缓存实现、栈和队列的底层实现等。

顺序表: 适用于需要随机访问数据的情景,因为顺序表的随机访问效率较高,可以直接通过索引访问任意位置的元素。例如,数组的底层实现、符号表等。

总结:

在选择数据结构时,需要根据具体应用场景权衡利弊。如果需要频繁插入和删除操作,并且对随机访问性能要求不高,则链表是一个不错的选择。如果需要随机访问数据,并且插入和删除操作不频繁,则顺序表是一个更好的选择。

;