数据结构之线性表

本文详细介绍了线性表的两种常见实现方式:顺序表和链表。顺序表包括静态和动态两种,前者使用固定大小数组,后者使用动态分配内存。链表分为单链表和双向链表,它们在存储和操作上各有特点,如链表支持快速插入删除但不支持随机访问。文章还提供了各种操作的实现代码,并分析了它们的时间复杂度和优缺点。
摘要由CSDN通过智能技术生成

前言
线性表是最常用且最简单的一种数据结构。简言之,一个线性表是n 个数据元素的有限序列。线性表在逻辑上是一种线性结构,也就是说连续的一条直线,但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组链式结构存储。

二、线性表

2.1顺序表

2.1.1顺序表的实现

  • 概念及结构
    顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构一般情况下采用数组存储。在数组上完成增删查改。
    在这里插入图片描述

  • 顺序表一般可以分为:
    1.静态顺序表:使用定长数组存储。

#define N 100
typedef int SQDataType;//定义数据类型
typedef struct SeqList
{
    SQDataType array[N];//数组长度
    size_t size;//有效数据的个数
}SeqList;

静态顺序表的初始化、增、删、查、改等基本操作:

void SeqListInit(SeqList* psl)//初始化,传地址
{
    memset(psl->a,0,sizeof(SQDataType)*N);
    psl->size=0;
}
//头插
void SeQListPushBack(SeqList* psl,SQDataType x)
{
    if(psl->size>=N)
    {
        printf("SeqList is Full");
        return;
    }
    psl->a[psl->size]=x;
    psl->size++;
}

问题:给少了不够用,给多了造成浪费
2.动态顺序表:使用动态开辟的数组存储。

typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* a;
	int size;//有效数据的个数
	int capacity;//容量		
}SL;

动态顺序表的初始化、增、删、查、改等基本操作:

void SeqListInit(SL* ps)
{
	ps->a = NULL;
	ps->size = 0;
	ps->capacity = 0;	
}
void SeqListPushBack(SL* ps, SLDataType x)//尾插
{
	SeqListCheak(ps);//检查是否需要扩容
	ps->a[ps->size] = x;
	ps->size++;
}
void SeqListPrint(SL* ps)//打印
{
	assert(ps->size>0);
	for (int i = 0;i < ps->size;++i)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}
void SeqListCheak(SL* ps)//检查是否需要扩容
{
	if (ps->size >= ps->capacity)
	{
		ps->capacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		ps->a = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity);
		if (ps->a == NULL)
		{
			printf("扩容失败\n");			
			exit(-1);
		}
	}
}
void SeqListPopBack(SL* ps)//尾删
{
	assert(ps->size>0);
	//ps->a[ps->size - 1];//无意义
	ps->size--;
}
void SeqListPushFront(SL* ps, SLDataType x)//头插
{
	SeqListCheak(ps);
	int end = ps->size-1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[0] = x;
	ps->size++;
}
void SeqListPopFront(SL* ps)//头删
{
	int start = 0;
	while (start<=ps->size-1)
	{
		ps->a[start] = ps->a[start+1];
		++start;
	}	
	ps->size--;
}
void SeqListInsert(SL* ps, int pos, SLDataType x)//任意位置的插入删除
{
	assert(pos<=ps->size);
	SeqListCheak(ps);
	int end = ps->size - 1;
	while (end >= pos-1)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[pos-1] = x;
	ps->size++;

}
void SeqListErase(SL* ps, int pos)
{
	assert(pos < ps->size);
	int start = pos-1;
	while (start <= ps->size - 1)
	{
		ps->a[start] = ps->a[start + 1];
		++start;
	}
	ps->size--;
}
void SeqListFind(SL* ps, SLDataType x)//顺序表查找
{
	for (int i = 0;i < ps->size;++i)
	{
		if (ps->a[i] == x)
		{
			printf("找到了,下标是%d \n", i);
			return i;
		}
	}
	return -1;
}
void SeqListSort(SL* ps)//顺序表的排序
{
	for (int i = 0;i < ps->size - 1;++i)
	{
		int flag = 1;
		for (int j = 0;j < ps->size - 1 - i;++j)
		{
			if (ps->a[j] > ps->a[j + 1])
			{
				int tmp = ps->a[j];
				ps->a[j] = ps->a[j + 1];
				ps->a[j + 1] = tmp;
				flag = 0;
			}
		}	
		if (flag == 1)
			break;
	}

}
void SeqListBinaryFind(SL* ps,SLDataType x)//二分查找
{
	int left = 0;
	int right = ps->size-1;
	int mid = 0;
	while (left <= right)
	{
		mid = (left + right) / 2;
		if (ps->a[mid] < x)
		{
			left = mid + 1;
		}
		else if (ps->a[mid]> x)
    	{
			right = mid -1;
		}
		else
		{
			printf("找到了下标是 %d \n",mid);
			break;
		}
	}
}
void SeqListDestroy(SL* ps)//顺序表摧毁
{
	free(ps->a);
	ps->size = 0;
	ps->capacity = 0;

}
  • 插入算法的时间复杂度分析:
    问题的规模为表的长度n。算法的时间复杂度主要花费在向后移动元素的循环语句上。该语句重复执行*(n-i+1)*次。

2.1.2顺序表刷题

问题一:
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

来源:力扣(LeetCode)

int removeElement(int* nums, int numsSize, int val)
{
    
        int res = 0;
        for(int i = 0 ; i < numsSize; i++)
        {
            if(nums[i] == val)
            {
                
            }
            else
            {
                nums[res] = nums[i];
                res++;
            }
        }
        return res;
}

思路分析:
用i遍历数组,用res指向第一个位置,如果不是val则把值给给res指向位置res++。
问题二:
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
    int* newNum=(int *)malloc(sizeof(int)*(m+n));
    int i=0;
    int j=0;
    int min=0;
    while(i<m&&j<n)
    {
        if(nums1[i]<nums2[j])
        {
            newNum[min++]=nums1[i++];             
        }
        else
            newNum[min++]=nums2[j++];
    }
    if(i<m)
    {
        while(i<m)
        {
            newNum[min]=nums1[i];
            i++;
            min++;
        }

    }
    if(j<n)
    {
        while(j<n)
        {
            newNum[min]=nums2[j];
            j++;
            min++;
        }
    }
    for(int k=0;k<m+n;++k)
    {
        nums1[k]=newNum[k];
    }

}

来源:力扣(LeetCode)

思路分析:
开辟一个新的数组numNum,依此取出num1,num2中较小者,给给numNum,最后将num1或num2剩余元素给给numNum。

2.1.3顺序表分析

  1. 缺陷:
    1.如果空间不够增容。会造成一定的性能消耗,其次会造出一定的空间浪费。
    2.头部或者中间插入删除效率低。

2.2链表

链表:

  1. 空间上,按需给空间
  2. 不要求物理空间连续
    在这里插入图片描述

2.2.1链表的概念及结构

概念:链表是一种物理存储结构上非连续、非顺序·的存储结构,数据元素的逻辑结构是通过链表中的指针链接次序实现的。
逻辑结构:
在这里插入图片描述

2.2.2单链表的实现及基本操作

  • 单链表
typedef int SListDataType;
typedef struct SListNode
{
	SListDataType data;
	struct SListNode* next;
}SListNode;
  • 基本操作
SListNode* BuySListNode(SListDataType x)//申请结点
{
	SListNode* NewNode = (SListNode*)malloc(sizeof(SListNode));
	if (NewNode == NULL)
	{
		printf("申请内存失败");
		exit(-1);
	}
	NewNode->data = x;
	NewNode->next = NULL;
	return NewNode;
}
void SLPushBack(SListNode** pphead,SListDataType x)//链表尾插法
{
	SListNode* NewNode = BuySListNode(x);
	if (*pphead == NULL)
	{
		*pphead = NewNode;
	}
	else
	{
		SListNode* tail = *pphead;//找尾
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = NewNode;
	}
}
void SLPushPop(SListNode** pphead)//链表尾删法
{
	if (*pphead == NULL)
	{
		return;
	}
	else if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SListNode* prev = NULL;
		SListNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}

}
void SListPrint(SListNode* phead)//遍历打印链表
{
	SListNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL");
	printf("\n");
}
void SListPushFront(SListNode** pphead, SListDataType x)//头插法
{
	SListNode* NewNode = BuySListNode(x);
	if (*pphead == NULL)
	{
		*pphead = NewNode;
	}
	else
	{
		//SListNode* head = *pphead;
		NewNode->next = *pphead;
		*pphead = NewNode;
	}
}
void SListPopFront(SListNode** pphead)//头删法
{
	if (*pphead == NULL)
	{
		return;
	}
	else if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SListNode* head = (*pphead)->next;
		free(*pphead);
		(*pphead)->next = NULL;
		*pphead = head;
	}
}
void SListDelete(SListNode** pphead,int i)//任意位置删除
{
	SListNode* prev = NULL;
	SListNode* cur = *pphead;
	int size = 0;
	while (size<=i)
	{
		prev = cur;
		cur = cur->next;
		++size;
	}
	assert(i >= 0 && i <= size);
	if (*pphead == NULL)
	{
		return;
	}
	else
	{
		prev = cur->next;
		free(cur);
		cur->next = NULL;
		
	}
}

单链表存在缺陷:
在这里插入图片描述

2.3双向链表

2.3.1双向链表基本介绍

带头结点,不需要改变传过来的指针,也就是意味着不需要传二级指针

2.3.2双向链表的实现

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;//尾指针
	struct ListNode* prev;//头指针
	LTDataType data;
}ListNode;

2.3.3双向链表的基本操作

void ListInit(ListNode** pphead)//初始化双向链表
{
	*pphead = BuyListNode(0);
	(*pphead)->next = *pphead;
	(*pphead)->prev = *pphead;
}
void ListPushBack(ListNode* phead, LTDataType x)//尾插
{
	assert(phead);//判断是否为空
	ListNode* tail = phead->prev;
	ListNode* newNode = BuyListNode(x);
	tail->next = newNode;
	newNode->prev = tail;
	newNode->next = phead;
	phead->prev = newNode;
}
ListNode* BuyListNode(LTDataType x)//创建新的结点
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	node->next = NULL;
	node->prev = NULL;
	node->data = x;
	return node;
}
void ListNodePrint(ListNode* phead)//打印链表
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	ListNode* tail = phead->prev;
	ListNode* tailPrev = tail->prev;
	free(tail);
	tail = NULL;
	phead->prev = tailPrev;
	tailPrev->next = phead;
}
void ListPushFront(ListNode* phead, LTDataType x)//头插
{
	assert(phead);
	ListNode* first = phead->next;
	ListNode* newNode = BuyListNode(x);
	phead->next = newNode;
	newNode->prev = phead;
	first->prev = newNode;
	newNode->next = first;
}
void ListPopFront(ListNode* phead)//头删
{
	assert(phead);
	assert(phead->next != phead);
	ListNode* first = phead->next;
	ListNode* second = first->next;
	phead->next = second;
	second->prev = phead;
	free(first);
	first = NULL;
}
ListNode* ListFind(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* posPrev = pos->prev;
	ListNode* newNode = BuyListNode(x);
	posPrev->next = newNode;
	newNode->prev = posPrev;
	newNode->next = pos;
	pos->prev = newNode;
}
void ListErase(ListNode* pos)
{
	assert(pos);
	assert(pos->next != pos);
	ListNode* posPrev = pos->prev;
	ListNode* posNext = pos->next;
	free(pos);
	pos = NULL;
	posPrev->next = posNext;
	posNext->prev = posPrev;
}
void ListDestory(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
	phead = NULL;
}

双向链表的结构虽然复杂了,但是操作反而简单了

2.3顺序表与链表分析

  • 顺序表:
    优点:
    空间连续、支持随机访问
    缺点:
    中间或前面部分插入时间复杂度O(n)
    增容代价较大

  • 链表(带头双向循环)
    缺点:以结点为储存单位,不支持随机访问
    优点:
    任意位置插入删除时间复杂度为O(1)
    没有增容消耗,按需申请节点空间,不用了直接释放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值