《大话数据结构》第三章 线性表

第三章 线性表

算法时间复杂度

时间复杂度:语句的总执行次数T(n)是一个关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。时间复杂度,也就是算法的时间量度,记:T(n) = O(f(n))。这样用大写O( )来体现算法时间复杂度记法,我们称之为大O记法。
大O记法推导:

  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数函数中,只保留最高阶项。
  3. 如果最高阶存在且不是1,则去除与这个项相乘的常数。得到的结果就是大O阶。

  所谓的算法复杂度,就是找执行次数与n的关系。

  • 顺序执行结构,不管f(n)是1次 ,还是5次,把所有加法常数换成常数1,没有最高阶项,所以时间复杂度是O(1) 。
  • 线性阶:for(int i = 0; i<n ;++){ 执行时间复杂度是1的步骤} , f(n) = n,因此时间复杂度是O(n)
  • 对数阶:int count = 1; while(count < n){ count = count*2; } ,需要执行次数log2n,时间复杂度是O(log2n)
  • 平方阶:嵌套两个线性阶循环,时间复杂度O(n2)

常见的时间复杂度:
          在这里插入图片描述
常用的时间复杂度所耗费时间从小到大:
    在这里插入图片描述

线性表

线性表的定义:零个或者多个数据元素的有限序列。线性表是最简单的一种数据结构,线性表元素个数定义为线性表的长度。每次幼儿园小朋友排队出门的次序、一年的星座排列、也是线性表。

顺序线性表结点插入与删除

  分析插入和删除的时间复杂度:最好情况时,插入或者删除最后一个元素,时间复杂度是O(1),若插入到第一个元素或者删除第一个元素,意味着要移动所有的元素向前或者向后,此时时间复杂度是O(n)。
  顺序线性表的数据结构

	struct SqList{
		ElemType data[MAXSIZE];
		int length;
	};

  以下为顺序链表的插入与删除:无论何种数据结构的删除与插入操作,首先要对传入的参数进行判断,更改的数据结构是否有意义、删除插入位置是否超出范围,特别要注意的是移动过程中不能使用超出数据结构最大范围的值,否则会出现异常

	#include<stdio.h>
	#define MAXSIZE 20      /*存储空间初始分配量*/
	typedef int ElemType;   /*ElemType类型根据实际情况而定,这里假定为int*/
	//线性表顺序存储结构 
	typedef struct
	{
		ElemType data[MAXSIZE]; //数组存储元素最大值为MAXSIZE 
		int length;				//线性表当前长度 
	}SqList; 
	
	#define OK 1
	#define ERROR 0
	#define TRUE 1
	#define FALSE 0
	typedef int Status;          //Status描述方法运行的状态  OK  ERROR
	/*
		SqList L:需要从L线性表中获取
		int i:第i个元素
		ElemType *e: 最终结果保存在*e中 
	*/
	Status GetElem(SqList L,int i,ElemType *e)
	{
		//无论什么方法先判断传进来的参数是否有效
		 
		if(L.length == 0 || i>L.length||i < 1)
		{
			return ERROR;
		}
		*e = L.data[i - 1];
		return OK;
	}
	/* 
		SqList L :需要向L线性表中插入 
		int i:在第i个位置
		ElemType e: 插入的元素是e 
		初始条件:  1<= i <= L.length 
	*/
	Status ListInsert(SqList *L,int i,ElemType e)
	{
		int k;
		//插入时判断线性表是否已满、插入的位置是否在范围内 
		if(L->length == MAXSIZE)
		{
			return ERROR;
		} 
		if(i < 1 || i > L->length+1)
		{
			return ERROR;
		}
		//若不初始化线性表第一个元素直接插入第一个位置会有问题 
		if(i <= L->length)
		{
			for(k = L->length -1; k >= i -1; k--)
			L->data[k+1] = L->data[k];
		}
		L->data[i - 1] = e;
		L->length ++;
		return OK;
	}
	 /* 
		SqList *L :需要在L线性表中删除 
		int i:第i个位置的元素 
		ElemType *e: 并用e返回其删除的值 
		初始条件:  1<= i <= L.length 
	*/
	Status ListDelete(SqList *L ,int i,ElemType *e) 
	{
		int k;
		//判断线性表是否为空  删除的位置是否超出范围
		if(L->length == 0)
			return ERROR;
	    if(i<1 || i> L->length) 
	    	return ERROR;
	   	*e = L->data[i - 1];
	   	if(i < L->length)
	   	{
	   		//从要删除的位置依次移位到线性表最大值
	   		for(k = i; k < L->length ;k ++)
			{
				L->data[k-1] = L->data[k]; 
			}	
		}
		L->length -- ;
		return OK;
	}
	int main()
	{
		int first ;
		SqList shunxuLinearTable ;
		shunxuLinearTable.length = 1;
		shunxuLinearTable.data[0] = 1;
		//测试线性表插入功能 
		if(ListInsert(&shunxuLinearTable,1,2) == ERROR) printf("insert error");
		if(ListInsert(&shunxuLinearTable,1,3) == ERROR) printf("insert error");
		//测试线性表读取	
		if(GetElem(shunxuLinearTable,1,&first) == ERROR) printf("get error");
		
		printf("hello\n");
		printf("first is :%d\n",first);
		ListDelete(&shunxuLinearTable,1,&first);
		printf("删除的first is:%d\n",first);
		if(GetElem(shunxuLinearTable,1,&first) == ERROR) printf("get error");
		printf("删除后的first is :%d\n",first);
		return 0;
	} 

运行结果:
      在这里插入图片描述

单链表结点插入与删除

  为解决顺序存储结构,插入和删除需要移动大量元素缺点,改用链式存储线性表。链式存储线性表(也称单链表)数据结构:

	typedef struct Node{
		ElemType data;
		struct Node *next;//注意结构体可以自引用结构体指针  ,不能struct Node next 
	}Node;
	typedef struct Node *LinkList; 

获得链式存储线性表第i个数据算法思路:

扫描二维码关注公众号,回复: 11479260 查看本文章
  • 1、声明一个节点p指向链表的第一个结点,初始化j从1开始
  • 2、当j<i时就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
  • 3、若链表末尾p为空,则说明第i个元素不存在
  • 4、若查找成功,则返回节点p的数据

单链表第i个数据插入节点算法思路:

  • 1、声明一个节点指向第一个节点,初始化j从1开始
  • 2、当j<i时就遍历链表,让p的指针向后移动,不断指向下一个节点,j累加1
  • 3、若链表末尾p为空,则说明第i个元素不存在
  • 4、若查找成功,在系统中生成一个空节点s
  • 5、将数据元素e赋值给s->data
  • 6、单链表的插入语句s->next = p->next; p->next = s
    注意:s->next = p->next; p->next两条语句顺序不可变,否则导致插入的s最终的next是自己,没有了上级。自己手写时,需要先画出p 、p->next、插入的结点s之间的关系图。
           在这里插入图片描述
    单链表第i个数据删除结点的算法思路:
    1、声明一个结点p指向链表第一个结点,初始化j从1开始
    2、当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加
    3、若到链表末尾p为空,则说明第i个元素不存在
    4、若查找成功,将于删除的结点p->next赋值给q
    5、单链表的删除标准语句p->next = q->next
    6、将q结点的数据赋值给e,作为返回
    7、释放q结点内存
    手写前先画出p 要删除的q节点之间的关系。
          在这里插入图片描述
      由于单链表的结构没有定义表长,所以不知道事先要循环多少次,只能尝试先循环将指针移到插入或者删除的位置,然后判断此时的结点是否有意义,有意义进行插入或者删除。
      
	#include<stdio.h>
	#include<stdlib.h>
	#define OK 1
	#define ERROR 0
	typedef int Status;          //Status描述方法运行的状态  OK  ERROR
	typedef int ElemType;
	typedef struct Node 
	{
		int data;
		struct Node *next;
	}Node;
	typedef struct Node *LinkList;
	
	/*
		LinkList L:需要从L线性表中获取
		int i:第i个元素
		ElemType *e: 最终结果保存在*e中 
	*/
	Status GetElem(LinkList L,int i,ElemType *e)
	{
		//因为不知道链式存储链表的长度所以第i个元素是否存在一下子不能判断 
		 if( !L ) return ERROR; 
		 
		 int j = 1;
		 LinkList p = L; //传入的链表头就当第一个结点 
	  	 while( p && j < i)
	  	 {
	  	 	p = p->next;
	  	 	j++;
	     }
	     if(!p || j>i)
	     {
	     	return ERROR;
	     }
	     *e = p->data;
		 return OK; 
	}
	/* 
		LinkList L :需要向L线性表中插入 
		int i:在第i个位置     1<= i <= 单链表的长度  当链表一个元素也没有时也不能插入第一个位置 
		ElemType e: 插入的元素是e 
	*/
	Status ListInsert(LinkList L,int i,ElemType e)
	{
		//插入时判断线性表是否已满、插入的位置是否在范围内
		//由于是单链表,长度没有固定,单链表长度不确定,只能先判断传入的参数是否有意义 
		if(!L ) return ERROR;
		int j = 1;
		LinkList p,s;
		p = L;
		while( p && j<i)
		{
			p = p->next;
			j++;	
		}
		if(!p ||j>i) //若第i个元素不存在 ,若插入的范围超出链表长度返回ERROR 
	    {
	 		return ERROR; 	
	 	}
	 	s = (LinkList) malloc(sizeof(Node));//malloc函数内存中找片空地,生成一个新节点 
	 	s->data = e;
	 	s->next = p->next;
	 	p->next = s;
		return OK;
	}
	 /* 
		LinkList L:需要在L线性表中删除 
		int i:第i个位置的元素 
		ElemType *e: 并用e返回其删除的值 
		初始条件:  1<= i <= L.length 
	*/
	Status ListDelete(LinkList L ,int i,ElemType *e) 
	{
		int j = 1;
		LinkList p,q;
		p = L;
		while(p -> next &&j < i)
		{
			p = p->next;
			j++;
		}
		if(!p || j>i)
		{
			return ERROR;
		}
		q = p->next;
		p->next = q->next;
		*e = q->data;
		free(q);
		return OK;
	}
	int main()
	{
			int first ;
			LinkList linkLinearTable = (LinkList)malloc(sizeof(Node)) ;
			linkLinearTable ->data = 1;
			linkLinearTable ->next = NULL;
			if(ListInsert(linkLinearTable,1,2) == ERROR) printf("insert error\n");
			if(ListInsert(linkLinearTable,1,3) == ERROR) printf("insert error\n");
			if(ListInsert(linkLinearTable,4,4) == ERROR) printf("insert error\n");
			if(GetElem(linkLinearTable,1,&first) == ERROR) printf("get error");
			printf("first element is :%d\n",first);
			//测试删除结点
			if(ListDelete(linkLinearTable,1,&first)  == ERROR) printf("delete error\n");
			printf("delete element is :%d\n",first);
			if(GetElem(linkLinearTable,1,&first) == ERROR) printf("get error");
			printf("after delete first is :%d\n",first);
			if(ListDelete(linkLinearTable,1,&first)  == ERROR) printf("delete error\n");
			printf("delete element is :%d\n",first);
			if(GetElem(linkLinearTable,1,&first) == ERROR) printf("get error\n");
			printf("after delete first is :%d\n",first);	
			return 0;
	}		

运行结果:
      在这里插入图片描述

单链表整表创建与删除

单链表整表创建的算法思路(头插法):

  • 1、声明一结点p和计数器变量i
  • 2、初始化一空链表L
  • 3、让L的头结点指针指向NULL
  • 4、循环(生成一新结点赋值给p ,随机生成一个数值赋值给p->data,将p插入头结点和前一新结点直接)
    头插法新建链表LinkList L关键在于:始终让L处于头结点,每次循环新增的一个结点处于第二个结点。

  头插法新建的单链表:第一个头结点是传入的L,书中给的头插法L->data是没有赋值的,L头结点是没有值的 .

	/*
		头插法:随机产生n个元素的值的单链表L 
		头插法新建的单链表:第一个头结点是传入的L,此处提供的头插法L->data是没有赋值
	*/
	void CreateListHead(LinkList L,int n)
	{
		LinkList p;
		int i;
		srand(time(0));
///		L = (LinkList) malloc(sizeof(Node));
		L->next = NULL;
		for(i = 0; i<n; i++)
		{
			p = (LinkList)malloc(sizeof(Node));
			p->data = rand()%100+1; //A=rand()%x+y;产生一个以y为下限,以x+y为上限的随机数,并把值赋给A。
			p->next = L->next;   //每次循环新创建的结点都是此次循环的第二个结点 
			L->next = p;  //每次循环结束 都让L作为单链表的头结点 
		} 
	}

  尾插法新建链表LinkList L关键在于:先让L赋值给r,然后始终让r处于尾结点,每次循环新增的结点处于倒数第二个。L是尾插法的链表头结点指针,在理解r->next = p; r = p; 中可能会有点问题,要将r想象成只是充当中介的作用,r如果最后不等于p,那么 下一轮循环中如何让这个p接着指向下一个新建的p。以第一次执行r->next = p; r = p; 为例,第一次执行r = L,ai-1的地址是L,r->next = p后,L指向p。r = p后,r的表示的是p的地址,而ai-1到a之间的引用关系已经完成,r就是充当完成ai-1到a引用关系的中介。
在这里插入图片描述

	/*
		尾插法:尾插法 关键在于将L赋值给r,而r始终充当链表的尾结点作用 
	*/
	void CreateListTail(LinkList L,int n)
	{
		LinkList p,r;
		int i;
		srand(time(0));
		r = L;
		for(i = 0; i<n; i++)
		{
			p  = (LinkList)malloc(sizeof(Node));
			p->data = rand()%100 +1;
			r->next = p;  //将新创建的结点作为倒数第二个结点 
			r = p; 		  //r始终是要作为最后一个结点 
		}
		r->next = NULL; 
	} 

单链表整表删除的算法思路:

  • 1、声明结点p和q
  • 2、将第一个结点赋值给p
  • 3、循环(将下一个结点赋值给q,释放p,将q赋值给p)
		/*
		显然传入的参数要为链表的头结点 
	*/
	Status ClearList(LinkList L)
	{
		LinkList p,q;
		p = L->next;
		while(p)
		{
			q = p->next;
			free(p);
			p = q;
		}
		L->next = NULL;
		return OK;
	} 

测试代码:

	int main()
	{
			int first ;
			LinkList linkLinearTable = (LinkList)malloc(sizeof(Node)) ;
			linkLinearTable ->data = 1;
			linkLinearTable ->next = NULL;
			if(ListInsert(linkLinearTable,1,2) == ERROR) printf("insert error\n");
			if(ListInsert(linkLinearTable,1,3) == ERROR) printf("insert error\n");
			if(ListInsert(linkLinearTable,4,4) == ERROR) printf("insert error\n");
			if(GetElem(linkLinearTable,1,&first) == ERROR) printf("get error");
			printf("first element is :%d\n",first);
			//测试删除结点
			if(ListDelete(linkLinearTable,1,&first)  == ERROR) printf("delete error\n");
			printf("delete element is :%d\n",first);
			if(GetElem(linkLinearTable,1,&first) == ERROR) printf("get error");
			printf("after delete first is :%d\n",first);
			if(ListDelete(linkLinearTable,1,&first)  == ERROR) printf("delete error\n");
			printf("delete element is :%d\n",first);
			if(GetElem(linkLinearTable,1,&first) == ERROR) printf("get error\n");
			printf("after delete first is :%d\n",first);
			//测试单链表头插法新增,头插法无法读取头结点的值 
			int second;
			LinkList  headLinkList =(LinkList) malloc(sizeof(Node));
		 	CreateListHead(headLinkList,10);
			if(GetElem(headLinkList,2,&second) == ERROR) printf("get headLinkList 单链表 error\n");
			printf("头插法得到的单链表 second is :%d\n",second);
			//测试单链表尾插法新增------尾插法读起来有些困难
			//测试删除头插法创建的链表
			 ClearList(headLinkList);
			if(GetElem(headLinkList,2,&second) == ERROR) printf("get删除后 headLinkList 单链表 fail\n");
			//测试尾插法 
		 	LinkList  tailLinkList = (LinkList)malloc(sizeof(Node));   // CreateListTail会给tailLinkList分配内存所以不用自己分配 
			 CreateListTail(tailLinkList,10);
			if(GetElem(tailLinkList,2,&second) == ERROR) printf("get tailLinkList 单链表 error\n");
			printf("尾插法得到的单链表 second is :%d\n",second);
			//测试单链表尾插法新增------尾插法读起来有些困难
			//测试删除尾插法创建的链表
			 ClearList(tailLinkList);
			if(GetElem(tailLinkList,2,&second) == ERROR) printf("get删除后 tailLinkList 单链表 fail\n");	  
			return 0;
	}

运行结果:
        在这里插入图片描述
单链表和顺序存储线性表优缺点:

空间性能 空间性能
单链表 插入和删除时间复杂度O(1) 不需要分配存储空间,元素个数不受限制
顺序存储线性表 顺序存储链表时间复杂度最多可以达到O(N) 需要预分配存储空间,分大了浪费,分小了容易溢出

静态链表

  在动态链表中,结点的申请和释放借用malloc()和free()两个函数。单链表创建时无论是头插法还是尾插法,个人感觉使用起来都有问题。而静态链表操作的是数组,需要手动模拟动态链表的存储结构申请malloc和释放free函数
静态链表的数据结构:

#define MAXSIZE 1000 //假设链表最大长度1000
typedef struct
{
	ElemType data;
	int cur; //游标 为0时表示无指向 
} Component,StaticLinkList[MAXSIZE];

  静态链表也要初始大小,为了方便插入不溢出,暂定链表最大长度为1000,StaticLinkList[MAXSIZE]数组的第一个元素StaticLinkList[0]的cur用于存放备用剩余链表结点的第一个结点的下标。存放的第一个位置数据是存放在StaticLinkList[1]中的。
    在这里插入图片描述
模拟malloc和free函数,需要不断改动StaticLinkList[0]中的cur。下面例子中,一开始插入了甲乙丁戊已庚,想在第三个位置插入丙,则Malloc_SSL开辟出的第一个空节点游标是7,space[7]存放丙,需要更改乙的游标指向7,丙的游标指向丁。

	#include<stdio.h>
	#define MAXSIZE 1000 //假设链表最大长度1000
	typedef char * ElemType;	
	#define OK 1
	#define ERROR 0
	#define TRUE 1
	#define FALSE 0
	typedef int Status;          //Status描述方法运行的状态  OK  ERROR
	typedef struct
	{
		ElemType data;
		int cur; //游标 为0时表示无指向 
	} Component,StaticLinkList[MAXSIZE];
	//初始化静态链表为空,最后一个元素的cur为0 
	Status InitList(StaticLinkList space)
	{
		int i;
		for(i = 0;i<MAXSIZE -1 ;i++)
		{
			space[i].cur = i+1;
		}
		space[MAXSIZE - 1].cur = 0;
	}
	//需要手动模拟malloc和free函数
	int Malloc_SSL(StaticLinkList space) 
	{
		int i = space[0].cur;  //space[0]存放一个备用的空闲的下标 
		if(space[0].cur)
			space[0].cur = space[i].cur;//空闲的备用下标被使用后,把它的下一个作为备用 
		return i;
	}
	void Free_SSL(StaticLinkList space,int k)
	{
		space[k].cur = space[0].cur; //将空闲的备用下标存放在要删除的结点中 
		space[0].cur = k;			 //将要删除的下标作为space[0]首选备用的下标 
	}
	//返回静态链表L中的数据元素个数
	int ListLength(StaticLinkList L)
	{
		int j = 0;
		int i = L[MAXSIZE -1].cur; //最后一个元素指向的下一个的下标 
		while(i)
		{
			i = L[i].cur;
			j++;
		}
		return j;
	}
	//获得静态链表第I个元素 
	ElemType getElement(StaticLinkList L,int i)
	{
		int k = MAXSIZE - 1;
		int j ;
		if(i <1 || i>ListLength(L))
			return ERROR;
		for(j = 0; j<i; j++)
			k = L[k].cur;
		return L[k].data;	
	} 
	Status ListInsert(StaticLinkList L,int i,ElemType e)
	{
		int j,k,l;
		k = MAXSIZE - 1;
		//判断插入的元素是否超出范围,静态链表可以知道链表的元素个数 
		if(i<1 || i>ListLength(L)+1)
			return ERROR;
		j = Malloc_SSL(L);   //获得空闲分量的下标
		if(j)
		{
			L[j].data = e;  //找个空着的结点存放要插入的元素 ,而不是用malloc分配内存开辟 
			for(l = 1; l<= i-1;l++)//遍历要插入的位置是放在哪个结点的游标中,每次从MAXSIZE-1开始遍历 
			{
				k = L[k].cur;
			}
			L[j].cur = L[k].cur;
			L[k].cur = j;
			return OK;
		} 
		return ERROR;
	}
	//默认space[0]为第一个结点位置,i为插入的位置 
	Status ListDelete(StaticLinkList L,int i)
	{
		int j,k;
		if(i <1 ||i >ListLength(L))
			return ERROR;
		k = MAXSIZE - 1;  //最后一个地方存放着首结点的下标 
		for(j = 1;j<i - 1;j++)//先找到要删除的位置前后的游标 
		{
			k = L[k].cur;
		}
		j = L[k].cur;
		L[k].cur = L[j].cur;
		Free_SSL(L,j);//释放要删除的位置 结点 
		return OK; 
		
	} 
	int main()
	{
		StaticLinkList staticLinkList;
		InitList(staticLinkList);
		ListInsert(staticLinkList,1,"甲");
		ListInsert(staticLinkList,2,"乙");
		ListInsert(staticLinkList,3,"丁");
		ListInsert(staticLinkList,4,"戊");
		ListInsert(staticLinkList,5,"已");
		ListInsert(staticLinkList,6,"庚");
		printf("第一个位置值:%s\n",getElement(staticLinkList,1));
		printf("第三个位置值:%s\n",getElement(staticLinkList,1));
		printf("第六个位置值:%s\n",getElement(staticLinkList,6));
		if(	ListInsert(staticLinkList,3,"丙") == ERROR) printf("插入丙失败"); 
		printf("插入后第三个位置值:%s\n",staticLinkList[3].data);
		printf("插入后第六个位置值:%s\n",getElement(staticLinkList,6));
		printf("插入后第七个位置值:%s\n",getElement(staticLinkList,7));
		//测试删除函数
		ListDelete(staticLinkList,6); 
	    printf("删除原本第六个位置后,新的第六个位置值:%s\n",getElement(staticLinkList,6));
		return 0;
	} 

运行结果:
    在这里插入图片描述
  静态链表在插入和删除操作,只需要修改游标,不要移动元素,但是还是需要遍历知道插入位置的游标在哪个结点上,夸大的说是在插入和删除时改进了移动了大量元素缺点,但是还是没有解决连续存储带来的表长问题,但是肯定是比连续线性表好用的。

猜你喜欢

转载自blog.csdn.net/weixin_41262453/article/details/87893182