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