《数据结构与算法》课程笔记 第二章 2.1 线性表(2)

1. 线性表的应用

1 合并线性表

  • 先分析性能,后再通过代码实现。
  • List_Insert:顺序存储和链式存储结构的时间复杂度最坏情况下都是 O(N)。但是若在头结点插入,顺序结构:O(N),链式结构:O(1);若在尾结点插入, 顺序结构:O(1),链式结构:O(N)。
  • 合计:顺序结构:O(N^{2}),链式结构:O(N^{2})
  • 虽然顺序存储结构编程简单,且其中有多步都是常数时间。但除了插入操作,其它操作时间都花在定位上,非常快,且顺序存储结构在插入操作时需要移动数据元素。所以最佳的选择是链式存储结构。

底层代码顺序存储结构的实现: 

Status List_Union(SqlListPtr La, SqlListPtr Lb)
{
	Status status = success;
	for(int i=1;i<=Lb->length;i++)
	{
		int j;
		for(j=1;j<=La->length;j++)
		{
			if(Lb->elem[i] == La->elem[j])
				break;
		}
		if (La->length < LIST_INIT_SIZE) //La有位置可以插入
		{
			if (j > La->length) //La中没有该元素
			{
				La->length++;
				int len = La->length;
				La->elem[len] = Lb->elem[i]; //在末尾插入(顺序存储结构在末尾插入方便)
			}
		}
		else
		{
			status = fail;
			break;
		}
	}
	return status;
}

使用基础操作:

Status List_Union(SqListPtr La, SqListPtr Lb)
{
	ElemType elem;          //存放从 Lb 中取出的元素
	Status status;          //状态代码
	int i,j;
	int len = List_Size(Lb);  //len 存放 Lb 的元素个数
	for(i=1;i<=len;i++)
	{
		List_Retrieve(Lb,i,&elem);  //取出 Lb 中第 i 个数据元素
		status = List_Locate(La,elem,&j);  //判断它是否在 La 中
		if (status!=success) //如果不在
		{
			status = List_Insert(La,1,elem); //插入到第一个位置(链式存储结构在第一个位置插入方便)
			if(status!=success)
				break;                       //插入失败则退出
		}
		else List_Add(La,j,1);          //La 的第 j 个数据加1
	}
	return status;
}

 2 合并有序表

Status List_Merge(SqListPtr La, SqListPtr Lb, SqListPtr Lc)
{
	ElemType elem1, elem2;
	status = List_Init(Lc);
	int i = 1, j = 1, k = 1;  //i,j,k分别用于指示 La,Lb,Lc中当前元素
	int n = List_Size(La);
	int m = List_Size(Lb);
	while(i<=n && j<=m)//两个表都还未处理完
	{
		List_Retrieve(La,i,&elem1);
		List_Retrieve(Lb,j,&elem2);
		if (elem1 < elem2)
		{
			status = List_Insert(Lc,k,elem1);
			i = i+1;
		}
		else
		{
			status = List_Insert(Lc,k,elem2);
			j = j+1;
		}
		k = k+1;
	}
	while(i <= n)//表La还未处理完
	{
		List_Retrieve(La,i,&elem1);
		status = List_Insert(Lc,k,elem1);
		i = i+1;
		k = k+1;
	}
	while(j <= m)//表Lb还未处理完
	{
		List_Retrieve(Lb,j,&elem2);
		status = List_Insert(Lc,k,elem2);
		j = j+1;
		k = k+1;
	}
	return status;
}

  • 链式存储结构效率更高:不需要再重新分配空间给第三个链表,直接使用原来的内存空间;不需要进行数据元素的拷贝。 

底层代码链式结构的实现: 

void merge_linklist(SqListPtr La, SqListPtr Lb, SqListPtr Lc)
{//la和lb为参与归并的两个有序表,lc为结果有序表
	Ptr pa,pb,pc,tp;
	pa = (*La)->next;
	pb = (*Lb)->next;
	pc = *La;
	while(!pa && !pb)
	{
		if (pa->data < pb->data)
		{
			pc->next = pa;
			pc = pa;
			pa = pa->next;
		}
		else if (pa->data > pb->data)
		{
			pc->next = pb;
			pc = pb;
			pb = pb->next;
		}
		else   //值相等的情况
		{
			pc->next = pa;
			pc = pa;
			pa = pa->next;
			tp = pb;
			pb = pb->next;
			free(tp);
			tp = NULL;
		}
	}
	pc->next = (pa ? pa : pb);  //插入剩余段
	free(Lb);
	Lc = La;
}

2. 栈和队列

  • 栈和队列是限定插入和删除只能在表的“端点”进行的线性表。 

  • 栈和队列是特殊的线性表,是插入、删除操作受限的线性表。  

2.1 栈

  •  栈是限定在表的同一端进行插入或删除操作的线性表。没有数据元素的栈称为空栈。
  • 进行插入或删除操作的一端称为栈顶,另一端称为栈底。
  • 插入数据元素的操作称为入栈,删除数据元素的操作称为出栈。
  • 栈的运算特性:后进先出(Last In First Out--LIFO)或先进后出。

2.1.1 栈的基本操作及测试

2.1.2 顺序存储结构栈

类型定义

typedef struct Stack
{
	int top; //就是线性表顺序存储结构的length(栈顶位置)
	StackEntry* elem; //动态分配空间大小为stack_size(一开始将总体空间都分配出来)
	int stack_size;  //能存储的总空间大小
}Stack,*StackPtr;
  •  域 elem[0,...,stack_size-1] 用于存放数据元素
  • 约定 top 用于存放栈顶元素的位置。top = -1 表示空栈;top = stack_size-1 表示栈满。 

  •  溢出:顺序栈的数据元素空间大小是预先分配的。当空间全部占满后再入栈产生的溢出称为“上溢”;当栈为空时再出栈也将产生的溢出称为“下溢”。

顺序栈基本操作实现:入栈操作 

Status Stack_Push(StackPtr s, StackEntry item)
{
	Status outcome = success;
	if(s->top == MAXSTACK-1)
		outcome = overflow; //栈满则上溢
	else
	{
		s->top++;
		s->elem[s->top] = item; //数据元素放入top位置(top先加1,再放数据元素)
	}
	return outcome;
}

 顺序栈基本操作实现:出栈操作 

Status Stack_Pop(StackPtr s, StackEntry *item)
{
	Status outcome = success;
	if(s->top == -1)
		outcome = underflow; //栈空则下溢
	else
		*item = s->elem[s->top--]; //将top所指数据元素放入item,top再减1。
		/**item = s->elem[s->top];
		s->top--;*/
	return outcome;
}

 顺序栈基本操作实现:取栈顶元素操作  

Status Stack_Top(StackPtr s,StackEntry *item)
{
	Status outcome = success;
	if(Stack_Empty(s))
		outcome = underflow; //栈空则下溢
	else
		*item = s->elem[s->top]; //取出数据,top指针不变
	return outcome;
}

2.1.3 链式存储结构栈

  • 单链表在链表头部进行插入和删除操作时比较简单,速度快。
  • 栈顶:单链表头部 

 类型定义:

typedef struct Node  //结点类型定义
{
	StackEntry entry;
	struct Node* next;
}StackNode, *StackNodePtr;

typedef struct Stack //链栈类型定义
{
	StackNodePtr top;  //指向栈顶的指针
}Stack, *StackPtr;
  • 空栈时 top == NULL 。

链栈基本操作实现:入栈操作  

Status Stack_Push(StackPtr s, StackEntry item)
{
	Status outcome = success;
	StackNodePtr np = MakeNode(item); //申请结点空间,并装填结点域
	/*StackNodePtr np = (StackNodePtr)malloc(sizeof(Node));
	np->entry = item;
	np->next = NULL;*/
	if(np == NULL)
		outcome = overflow; //无法分配存储空间,相当于栈满上溢
	else 
	{
		np->next = s->top; //所申请到的结点插入在表头(top指针指向链栈的第一个数据元素,最新插入的都放在表头)
		s->top = np; //top指向栈顶元素,现在栈顶元素变成 np 指向的元素
	}
	return outcome;
}

链栈基本操作实现:出栈操作   

Status Stack_Pop(StackPtr s, StackEntry *item)
{
	Status outcome = success;
	if(Stack_Empty(s))
		outcome = underflow; //栈空则下溢
	else
	{
		StackNodePtr np = s->top; //删除栈顶元素
		s->top = np->next;
		*item = np->entry;
		free(np);
	}
	return outcome;
}

链栈基本操作实现:取栈顶元素操作    

Status Stack_Top(StackPtr s, StackEntry *item)
{
	Status outcome = success;
	if(Stack_Empty(s))
		outcome = underflow; //栈空则下溢
	else 
		*item = s->top->entry;  //s->top是结点类型
	return outcome;
}
  • 链栈:入栈、出栈、取栈顶的时间复杂度都是 O(1)。不受顺序栈必须预估空间大小的限制。

  • 入栈:i、j、k。不可能的出栈顺序:k、i、j。
  • 括号配对问题也使用栈来实现,后出现先配对。

 2.2 队列

  • 队列是限定只能在表的一端进行插入,在表的另一端进行删除的线性表。
  • 队尾(rear):允许插入的一端
  • 队头(front):允许删除的一端
  • 队列特点:先进先出(FIFO) 

2.2.1 队列的基本操作

  • 链式结构:入队操作定位时间较长。顺序结构:出队操作需要大量移动数据。

2.2.2 顺序队列

顺序队列类型定义: 

typedef struct Queue
{
	int front,rear; //队头和队尾指针,指示队头和队尾数据元素的位置
	QueueEntry entry[MAXQUEUE]; //数据元素存储空间
}Queue, *QueuePtr;  //定义为新的数据类型
  • 初始 front = rear = 0 或者 front = rear = -1 都是可以的。
  • 如果 front = rear = 0,表示 front 指向第一个元素,rear 指向最后一个元素的后面;
  • front = rear = -1,表示 front 指向第一个元素前面,rear 指向最后一个元素。
  • 这里规定:front = rear = -1,即 front 指向第一个元素前面一个位置,rear 指向最后一个元素的位置。

        

  • 解决“假溢出”现象:视为“循环顺序队列”。
  • 实现方法:front = (front+1)%MAXQUEUE;    rear = (rear+1)%MAXQUEUE;
  • 队满和队空的判断条件都是:s->front == s->rear; 如何解决判满和判空的问题?

循环顺序队列判断满还是空:

  • 本质上解决:循环队列满的本质为队列长度和空间大小相同;空的本质为队列长度为0。所以可以通过一个计数器的值是0还是空间大小来判断是满还是空。
  • 产生的原因:循环队列满的原因是入队操作产生的;空的原因是出队操作产生的。所以可以通过一个标记是入队还是出队来判断是满还是空。
  • 不允许出现:让一种情况不出现,如让空间大小还有一个位置时就认为是满的。
  • 优先选用第三种方案,不需要额外的变量。

      

2.2.3 链式队列

链式队列类型定义: 

typedef struct Node //链式队列的结点结构
{
	QueueEntry entry;  //队列数据元素类型
	struct Node* next; //指向后继结点的指针
}QueueNode, *QueueNodePtr;

typedef struct Queue //链式队列
{
	QueueNode *front; //队头指针
	QueueNode *rear;  //队尾指针
}Queue, *QueuePtr;

链式队列基本操作实现:初始化操作

void Queue_Init(QueuePtr q)
{
	QueueNode hNode;
	hNode = (QueueNode)malloc(sizeof(QueueNode));
	q->front = &hNode;
	q->rear = &hNode;
}

  • 最后一个元素出队时,增加一个操作:Q->rear = Q->front; 

猜你喜欢

转载自blog.csdn.net/sinat_35483329/article/details/86080526