数据结构:线性表

线性表是n个具有相同特性的数据元素的有序序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...

线性表在逻辑上是线性结构,也就是说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存在,通常以数组和链式结构的形式存储。

1.顺序表

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删改查。(顺序表是在数组的基础上还要求数据是连续存储的,不能跳跃间隔)

1.1建立静态顺序表

我们用结构体定义一个静态的顺序表,它的大小不可变。结构体中第一个成员变量SLDataType a[N];定义了顺序表在内存中是以数组的形式存储(与数组的区别是它必须按照内存块的顺序从前到后存,而数组可以任意存储某个块的值),宏定义N定义了数组的大小;在开始对int类型重命名为SLDataType,便于后续改变顺序表存储数据的类型。对于静态顺序表我们不作实现。

#define N 1000
typedef int SLDataType;//宏定义存储数据的类型

typedef struct SeqList
{
	SLDataType a[N];//以数组的形式存储在内存中
	int size;//表示数组中存储了多少个数据
}SL;

1.2建立和实现一个动态顺序表

我们在静态顺序表的基础上将静态的数组改为动态的可重新申请空间的指针变量,同时增加成员变量capacity(表示当前顺序表的最大容量),每当size(当前容量)伴随着新数据的存储增大直至等于容量时,这个时候我们就需要扩容。
接下来我们就会实现几个接口函数来完成对顺序表的操作。注意要完成对结构体内容的更改,那我们在定义函数参数时就应该传递创建好的结构体变量的地址进行传址调用,如果传值调用就不会对结构体变量的内容有任何改变。

typedef int SLDataType;//宏定义存储数据的类型
//动态顺序表
typedef struct SeqList
{
	SLDataType* a;
	int size;//表示数组中存储了多少个数据
	int capacity;//数组实际能存数据的空间容量是多大
}SL;
//接口函数
void SeqListPrint(SL* ps);//打印顺序表
void SeqListInit(SL* ps);//初始化顺序表
void SeqListPushBack(SL* ps, SLDataType x);//用尾插法给顺序表尾部添加一个元素
void SeqListPopBack(SL* ps);//删除尾部的最后一个元素
void SeqListPushFront(SL* ps, SLDataType x);//用头插法给顺序表头部添加一个元素
void SeqListPopFront(SL* ps);//删除头部的第一个元素

1.2.1实现打印顺序表

void SeqListPrint(SL* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}

1.2.2初始化顺序表

void SeqListInit(SL* ps)
{
	ps->a = NULL;
	ps->size = ps->capacity = 0;
}

1.2.3 释放内存

为了防止内存泄露,我们在使用完空间后要释放掉。

void SeqListDestory(SL* ps)
{
	free(ps->a);
	ps->a = NULL;
	ps->size = ps->capacity = 0;
}

1.2.4尾插法添加新元素 

尾插法就是添加新元素时将其插入到表尾作为最后一个元素。在顺序表中添加新数据时,我们必须先进行判断表容量此时是否已满,当size增长到与capacity相等时表明此时表已满需要进行扩容,在扩容之前需要注意的是,初始化之后的指针ps->a此时是空指针还没有指向,同时size=capacity=0,所以如果判断是空的顺序表初始化新容量为4,如果是顺序表原容量已满则将其容量增至两倍;接下来将ps->a指针所指向的空间扩容之后令tmp指向它(通过判断tmp是否为空判断是否扩容成功),更新成员变量。

void SeqListPushBack(SL* ps, SLDataType x)
{
	if (ps->capacity == ps->size)//顺序表已满或者顺序表为空
	{
		int newcapacity = ps->capacity == 0 ? 4 : (ps->capacity) * 2;
		SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapacity * sizeof(SLDataType));
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);//异常退出
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
	ps->a[ps->size] = x;
	ps->size++;
}

1.2.5删除表末尾的元素 

在删除时我们要注意判断表中元素是否已经被删完为空,如果不做判断易造成越界访问。可以在前面进行断言也可以通过if条件判断。

void SeqListPopBack(SL* ps)
{
	//assert(ps->size>0);
	if (ps->size == 0)
	{
		printf("顺序表已为空!\n");
	}
	else
		ps->size--;
}

1.2.6头插法添加新元素

头插法在插入数据时只能将原数据每个向后挪一个位置,再把新的数据添加在数组第一个位置。

void SeqListPushFront(SL* ps, SLDataType x)
{
	SeqCheckCapacity(ps);//将增容部分托管到了这个函数
	int end = ps->size - 1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[0] = x;
	ps->size++;
}

1.2.7头删法删除顺序表第一个元素

同样需要判断顺序表此时是否为空,将除了第一个元素的其余元素前移一个单位覆盖掉第一个元素。注意覆盖时循环结束条件。(当然此处也可以借助memmove函数)

void SeqListPopFront(SL* ps)
{
	//assert(ps->size > 0);
	int begin = 0;
	while (begin < ps->size-1)
	{
		ps->a[begin] = ps->a[begin+1];
		begin++;
	}
	ps->size--;
}

1.2.8在顺序表中查找某个元素

找到了返回该元素下标,没找到返回-1

int SeqListFind(SL* ps, SLDataType x)
{
	int i = 0;
	for (i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
			return i;
	}
	return -1;
}

1.2.9 在指定下标位置插入元素

void SeqListInsert(SL* ps, int pos, SLDataType x)
{
	assert(pos < ps->size &&  pos >= 0);//断言报错pos的非法性
	SeqCheckCapacity(ps);//将增容部分托管到了这个函数
	int i = 0;
	for (i = ps->size; i >pos; i--)
	{
		ps->a[i] = ps->a[i - 1];
	}
	ps->a[pos] = x;
	ps->size++;
}

1.2.10删除指定位置的元素

void SeqListErase(SL* ps, int pos)
{
	assert(pos < ps->size && pos >= 0);//断言报错pos的非法性
	if (ps->size != 0)
	{
		for (int i = pos; i < ps->size-1; i++)
		{
			ps->a[i] = ps->a[i + 1];
		}
		ps->size--;
	}
}

 1.3顺序表的缺陷

-空间不够了需要扩容,realloc扩容时分为原地扩容和异地扩容;原地扩容代价较小不需要拷贝数据,异地扩容代价较大需要拷贝原数据。
-避免频繁扩容,将空间设的较大会造成一定的空间浪费。
-顺序表要求数据从开始位置连续存储,那么在头部或者中间位置插入数据时就需要挪动数据,效率不高。

2.单链表

-针对顺序表的缺陷,设计出链表。按需申请空间,不用了就释放空间(更合理的使用了空间)。头部中间插入删除数据,不需要挪动数据,不存在空间浪费。
-但是链表也有缺陷,每存一个数据,都要存一个指针去链接后面的数据节点,不支持随机访问(用下标直接访问第i个);有些算法,需要结构支持随机访问;比如:二分查找、优化的快速排序。


2.1实现单链表 

2.1.1定义单链表节点

需要注意的是,当接口函数只需要调用不需要修改结构体指针即可完成操作时(比如打印单链表)进行传值调用,只需要将定义的结构体指针值传递,函数形参选择结构体指针即可;当接口函数要对实参进行修改(如头插法尾插法),此时要传递创建好的结构体指针的地址,同时接口函数形参用二级指针接收。--指针变量的传值调用与传址调用

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;//存储节点数据
	struct SListNode* next;//存储下一个节点的地址
}SLTNode;
//接口函数
void SListPrint(SLTNode* phead);//打印单链表
void SListPushBack(SLTNode** pphead, SLTDataType x);//尾插法
void SListPushFront(SLTNode** pphead, SLTDataType x);//头插法
void SListPopBack(SLTNode** pphead);//尾删法
void SListPopFront(SLTNode** pphead);//头删法
SLTNode* SListFind(SLTNode* pphead, SLTDataType x);//查找到可以修改
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在pos前插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);//在pos后插入
void SListErase(SLTNode** pphead, SLTNode* pos);//删除
void SListDestroy(SLTNode** pphead);//销毁

2.1.2打印单链表

void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

2.1.3尾插法添加新节点

先申请一个新节点,对其初始化。当链表为空时,令头节点直接指向新节点;当链表非空时,令其递归找下一个节点知道找到最后一个节点,令其指向新节点。

void SListPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuyListNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

2.1.4头插法添加新节点

申请一个新节点,进行初始化。令新节点指向头节点的指向,然后令头节点指向新节点。当链表为空时,这种做法同样可取。

void SListPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuyListNode(x);
	newnode->next = *pphead;//让新节点的下一个为头节点
	*pphead = newnode;//头节点指向新节点的位置
}

2.1.5尾删最后一个节点

删除单链表中的最后一个节点,我们需要找到最后一个节点,将它的内存释放掉置空,再将链表倒数第二个节点的next的成员变量内容置空;所以分为三种情况:第一种链表为空,此种情况直接进行断言处理掉;第二种链表只存在一个节点,没有前驱节点,直接将这个节点释放掉置空处理;当链表有两个及以上节点,就将最后一个节点内存释放置空,令其前一个节点指向空即可。

void SListPopBack(SLTNode** pphead)
{
	assert(*pphead != NULL);//链表已经为空
	if ((*pphead)->next == NULL)//链表就剩一个节点
	{
		free(*pphead);
		*pphead = NULL;
	}
	else//链表有两个及两个以上的节点
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

2.1.6头删第一个节点

删除单链表第一个节点。需要另一个结构体指针ptr先指向第二个节点,然后释放头节点所指向的第一个节点再置空,重新令头节点指针指向ptr所存的第二个节点。头删依旧需要判断链表是否已经为空,但是剩一个节点的情况上列做法依旧能处理。

void SListPopFront(SLTNode** pphead)
{
	assert(*pphead != NULL);
	SLTNode* ptr = (*pphead)->next;
	free(*pphead);
	*pphead = ptr;
}

2.1.7查找单链表中某个元素

在单链表中查找某个元素可以应用于查找后修改它,删除,在其之后增加;如果遍历查找时单链表中有重复的元素待查找,可以在测试函数中写一个循环,我们令查找函数在查找完后返回该位置的地址,那么下次循环可以从该地址的next开始,知道传回来的地址为空也就是单链表遍历完,至此就找到了单链表中所有的x。 

SLTNode* SListFind(SLTNode* pphead, SLTDataType x)
{
	SLTNode* cur = pphead;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		else
			cur = cur->next;
	}
	return NULL;
}

2.1.8在某个节点后面插入新节点

我们在借助查找函数找到的pos节点后插入新节点,在pos后直接插入新节点比较简便,单链表在查找前驱节点时很不方便需要从头遍历到pos前 ,所以我们一般选择在pos后插入新节点。

void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	SLTNode* newnode = BuyListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

2.1.9删除单链表中的某个节点

在删除单链表的某个节点之前,我们必须遍历找到它的前驱节点,让其指向它的后继节点,才能对该节点的空间进行释放置空。当需要删除的节点是链表的头结点时,需要令头节点指向该结点的下一个才能进行删除。

void SListErase(SLTNode** pphead, SLTNode* pos)
{
	if (pos == *pphead)
	{
		*pphead = pos->next;
		free(pos);
		pos = NULL;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

2.1.10销毁单链表

单链表的存储结构与顺序表不同,顺序表是一块连续的存储空间,一次就可以将空间全部释放;而单链表以单个节点的形式存储,每次只能释放一个节点的空间。

void SListDestroy(SLTNode** pphead)
{
	assert(pphead);
	while ((*pphead)->next != NULL)
	{
		SListPopBack(&*pphead);
	}
	SListPopFront(&*pphead);
}

2.2单链表OJ题练习

2.2.1反转链表. - 力扣(LeetCode)

--解题技巧:只要将所有的指针的指向反转即可,为了实现将节点的指向箭头反转,我们仅需要改变节点next域所存的地址,让所有next域存的下一个节点的地址改为存上一个节点的地址。

让1节点的next域存NULL,再让2的next域存1...,让5的next域存4。进行遍历,我们必须有一个指针一存着上一个节点的地址,一个指针二表示要改变域的当前节点,还有一个指针三得指向下一个要遍历的节点。指针一所存的地址赋给指针二的next域后,第一个节点的指向改变完成;接下来改变第下个节点,将上个节点的地址(也就是指针二指向的)要给到下个节点(指针三指向的)的next域中,更新指针指向:把上次指针二指的也就是上个节点赋给指针一,把上次指针三指的也就是这个节点赋给指针二,令指针三指向自己的下一个节点,继续完成将指针一所存的地址赋给指针二的next域。也就是说一直保证指针一二三指向相邻的三个节点,当完成第二个节点与第三个节点断掉再令其反转指向第一个节点操作后,我们仍旧可以通过指针三所存的断开的后续链表的头节点继续处理。当指针二指向最后一个节点且完成反转操作时结束,结束条件是指针二不为空,为空则已经处理完。当指针三为空时,就要注意不能让它再指向下一个。

struct ListNode* reverseList(struct ListNode* head) {
   if(head == NULL)
   {
    return NULL;
   }
   struct ListNode* n1 = NULL;
   struct ListNode* n2 = head;
   struct ListNode* n3 = head->next;
   while(n2)
   {
    n2->next = n1;
    n1 = n2;
    n2 = n3;
    if(n3)
    {
         n3 = n3->next;
    }
   }
   return n1;
}

2.3其它

结构复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然复杂,但是使用代码实现后会发现结构会带来很多优势,实现反而更加容易,后面我们将用代码实现。

顺序表和链表比较:

顺序表在头部中部插入删除时间效率低。O(n);连续的物理空间,空间不够了以后需要增容;增容有一定程度消耗;为了避免频繁增容,按照倍数去增,用不完可能存在一定的空间浪费。
链表在任意位置插入删除效率高。O(1);按需申请释放空间。但是链表不支持随机访问(用下标访问),意味着一些算法(二分查找、快排)等在这种结构上不适用。链表存储一个值同时要存储链接指针,也有一定消耗。CPU高速缓存命中率更低(寄存器在读取时会把数据加载到内存之后再访问,不命中就一直加载相邻空间知道数据被加载到缓存 )。 

3.栈的概念

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守先进后出的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
要实现栈,可以选用数组栈或链式栈。
链式栈:如果用尾做栈顶,尾插尾删,要删除最后一个节点时同时将前一个节点指针域置空,要设计成双向循环链表,否则删除数据效率低。如果用头做栈顶,头插头删,就可以设计成单链表。 
数组栈:空间不够时需要重新申请一段空间。我们一般选用数组栈。

4.队列的概念

队列先进先出的原则只能选用链表来实现,用数组实现先进先出时出队列操作比较复杂。

5.循环队列的概念

在实现循环队列时,可以用数组也可以用循环单链表。此时用数组实现时,记录指向对头的下标和指向队尾的下标,出队入队时改变队头队尾下标即可。正常情况下,当队头队尾相等时,队列为空或者队列已满,为了区分这两种情况,多增设一个数组元素大小或者单链表节点,当队头队尾相等时队列为空,当(tail+1)%(k+1)==front时,队列已满;为了确保数组循环,入队列tail自增时也得和数组长度取余,出队列front自增时同理。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值