考研数据结构——栈和队列(最全!)

一、栈

1.1栈的定义和基本概念

        要讨论一种数据结构,就要从三方面讨论:逻辑结构、数据的运算、存储结构(物理结构)。

1.1.1 定义

        线性表:线性表是具有相同数据类型的n个数据元素的有限序列,其中n为表长,当n=0时为空表。

        栈(stack):栈是 只允许在一端进行插入或删除操作 的线性表,即只能从栈尾插入删除。

        重要术语:栈顶、栈底、空栈。LIFO:last in first out,即后进先出

        所以栈的逻辑结构和普通线性表没有差别,数据操作上插入和删除操作有区别。

1.1.2 基本操作

        先回顾一下线性表的基本操作:

        栈的基本操作:

1.1.3 常考题型

若进栈顺序为“a-b-c-d-e”,则出栈顺序有哪些?

        由于进栈顺序有可能和出栈操作交叉进行,所以出栈顺序有多种。若有n个不同元素进栈,出栈顺序的不同排列个数为:

即“卡特兰数”。

        所以考试时几乎不太可能让写出所有的出栈顺序,一般会出选择题,还是比较简单的。

1.2 栈的顺序存储实现

        顺序栈就相当于顺序链表,用静态数组方式实现,是顺序存储。所有顺序栈的基本操作(创增删查)的时间复杂度都是O(1)。

        缺点:大小不可变。

1.2.1 初始化顺序栈

#include<iostream>
using namespace std;
#define MaxSize 10
//定义顺序栈
typedef struct {
	int data[MaxSize];		//静态数组存放栈中元素
	int top;				//栈顶指针
}SqStack;

//初始化栈
void InitStack(SqStack& S) {
	S.top = -1;				//初始化栈顶指针
}

//判断栈空
bool StackEmpty(SqStack S) {
	if (S.top == -1)
		return true;		//栈空
	else
		return false;		//栈不空
}

void test(){
    SqStack S;
    //操作
}

代码解析:

  • top是栈顶指针,指向栈顶元素,若栈中有四个元素,则top=4,即栈顶元素的下标。

  • 刚开始将S.top置为-1,因为初始化的顺序栈里面没有数据,data[0]没有数据,没有“栈顶元素”。

  • 在声明一个栈的时候分配内存使用的是变量声明的方式,没有使用malloc函数,在代码执行结束后系统会自动回收这块内存空间。

顺序存储:给各个数据元素分配连续的存储空间,大小为:MaxSize*sizeof(int)。

创建好之后在内存中是这个样子:

1.2.2 进栈操作

//新元素入栈
bool Push(SqStack& S, int x) {
	if (S.top == MaxSize - 1)
		return false;
	S.top = S.top + 1;
	S.data[S.top] = x;
	return true;
}

        注意:S.top 充当数组下标,是一个 int 型。

1.2.3 出栈操作

//出栈操作
bool Pop(SqStack& S, int& x) {
	if (S.top == -1)
		return false;
	x = S.data[S.top];		//x存储栈顶元素,即要出栈的元素
	S.top = S.top - 1;		//指针-1
	return true;
}

代码解析:

  • 先判断栈是否为空,若为空直接返回false;

  • 让 x 保存需要出栈的元素,即栈顶元素,并带回到函数调用者那里;

  • 令栈的指针 -1,只是在逻辑上删除了该元素,但是它的数据还残存在内存中。

1.2.4 读取栈顶元素

//读取栈顶元素
bool GetTop(SqStack S, int& x) {
	if (S.top == -1)
		return false;
	x = S.data[S.top];		//x存储栈顶元素,即要出栈的元素
	return true;
}

        代码解析:读取栈顶元素的代码和出栈操作的代码几乎一模一样,只是不需要将栈顶指针 -1,只要把被删元素 x 带回即可。

1.2.5 共享栈

        顺序栈的缺点:大小不可变。

        为了解决这个缺点,可以采用共享栈的方式。即两个栈共享同一片存储空间。

        共享栈的优点:节省存储空间,降低发生上溢的可能

        上溢和下溢,对栈而言:栈满还存为上溢,栈空再取即下溢。上溢和下溢都修改了栈之外的内存,因此有可能导致程序崩溃。

//共享栈
typedef struct {
	int data[MaxSize];		//静态数组存放栈中元素
	int top0;				//0号栈栈顶指针
	int top1;				//1号栈栈顶指针
}ShStack;
//初始化栈
void InitStack(ShStack& S) {
	S.top0 = -1;			//初始化栈顶指针
	S.top1 = MaxSize;
}

        刚开始的指针情况如图:

代码解析:

  • top0 从下往上存,top1 从上往下存,在逻辑上实现了两个栈的存取,可以提高内存空间的资源利用率。

  • 判断栈满的条件为:top0+1==top1

1.3 链栈

        链栈和单链表几乎一模一样,对应的基本操作(创增删查)也几乎相同,只是进栈/出栈只能在栈顶一端进行(链头作为栈顶)。链栈也分为带头结点和不带头结点,下面我 主要用不带头结点的方式 实现一遍链栈的操作。

1.3.1 链栈的定义

//链栈定义
typedef struct LinkNode {
	int data;
	struct LinkNode* next;
}*LiStack;

1.3.2 链栈的初始化(创)

//栈的初始化
void InitStack(LiStack& S) {
	S = NULL;		//设置栈顶指针为空
}

        若带头结点则改为“S.next = NULL”。

1.3.3 链栈的入栈(增)

//链栈的插入
bool Push(LiStack& list, int x) {
	SNode* s = (SNode*)malloc(sizeof(LiStack));
    // 内存分配失败的情况
    if(s == NULL) {
        return false;
    }
	s->data = x;
	s->next = list;
	list = s;
}

        先申请一个指针 s ,让 s 的 data 域保存要入栈的元素 x ,然后将 s 的 next 链到 list ,即将 s 插入到 list 前面,最后移动 list 的位置让其始终指向栈顶元素。

1.3.4 链栈的出栈(删)

//链栈的出栈
bool Pop(LiStack& list, int& x) {
	if (list == NULL)
		return false;
	//出栈结点是栈顶指针指向的结点
	x = list->data;							//将出栈节点的值赋给x
	SNode* p = (SNode*)malloc(sizeof(LiStack));		//临时结点,用于之后释放空间
	list = list->next;						//使头指针指向下一结点
	free(p);
	return true;
}

        要注意在使用 free 函数释放内存空间的时候,前面创建内存空间的时候一定要用 malloc 函数创建(这是c语言的方法);如果使用c++的方法,就是前面用 new 创建,后面用 delete 删除。

1.3.5 链栈的查找(获取栈顶元素)

//获取栈顶元素
bool GetPop(LiStack& list, int& e) {
	if (list == NULL)	//判空
		return false;
	e = list->data;		//用e保存当前要出栈的元素值
	return true;
}

1.3.6 其他基本操作

//1.输出栈的元素
void PrintS(LiStack list) {
	SNode* s = (SNode*)malloc(sizeof(LiStack));
	s = list;
	while (s != NULL) {
		cout << s->data << " ";
		s = s->next;
	}
	cout << "成功输出!" << endl;
}
//2.判空操作
bool Empty(LinkStack S) {
    // 栈为空的条件即栈顶指针指向NULL
    if(S == NULL)
        return true;
    return false;
}

二、队列

2.1 队列的定义和基本概念

2.1.1 定义

        队列是一种操作受限的线性表,即只允许在一端进行插入(入队),在另一端进行删除(出队)的线性表

        队列是先进先出(FIFO:first in first out)

2.1.2 基本操作

2.2 队列的顺序存储实现

2.2.1 初始化顺序队列

//定义一个顺序队列
typedef struct {
	int data[MaxSize];		//静态数组存放队列元素
	int front, rear;		//队头指针和队尾指针
}SqQueue;
//初始化队列
void InitQueue(SqQueue& Q) {
	//初始时,队头队尾指针指向0
	Q.rear = Q.front = 0;
}
//判空
bool Empty(SqQueue Q) {
	if (Q.front == Q.rear)
		return true;
	return false;
}

        队头指针指向队头,队尾指针指向队尾的后一个位置,即下一个要插入元素的位置。初始化时让队头队尾指针同时指向0,就代表一个空队列。

2.2.2 入队操作(循环队列)

//入队(错误示范)
bool EnQueue(SqQueue& Q, int x) {
	if (队列已满)
		return false;
	Q.data[Q.rear] = x;
	Q.rear = Q.rear + 1;
	return true;
}

        由于队列是顺序存储结构,内存固定不变,所以要先判断队列是否已满,如果没满就进行入队操作,最后将队尾指针向后移一位。这是“错误示范”的思路。

        至于错到了哪儿,我们要先想一下如何判断队列已满?是判断 Q.rear = MaxSize 吗?好像不太对,如果正常情况下队列已满之后,有一些元素从队头出队了,那这时候 Q.rear = MaxSize 仍然成立,但是队列并不是满的,再次有元素入队的话可以放到前面去。这是不是有点循环的味道了,这就对了。基于此,我们改进代码为 Q.rear = (Q.rear + 1) % MaxSize; 这样的话,每次队列满的时候,Q.rear 都会重新指向 data[0] 的位置:比如一个顺序队列最大容量为 10 ,某时刻 Q.rear指向队尾,即Q.rear=9,此时如果有新的元素入队,根据改进后的代码,得 Q.rear=(9+1)%10 ,即 Q.rear=0。

        那么我们还剩一个问题:判断队列已满的条件是什么?有同学可能要说“那当然是 Q.rear == Q.front” 啊,队头指针和队尾指针指向同一个位置的话就说明队列已满。但是你想一想我们的“判空”操作的判断条件是什么,在“判空”操作中,我们说当 Q.front==Q.rear 时就认为队列为空,那如果按照前面说的这种判定方法,是不是就有歧义了?所以我们的判定条件要改为 (Q.rear + 1) % MaxSize == Q.front ,即如果队尾指针的下一个位置指向队头,就说明队列已满(如下图),所以我们 必须牺牲一个存储单元来实现判断队列已满 的操作。

         于是,我们得到了正确的入队操作的代码:

//入队
bool EnQueue(SqQueue& Q, int x) {
	if ((Q.rear + 1) % MaxSize == Q.front)	//判断队列已满
		return false;		
	Q.data[Q.rear] = x;						//新元素插入队尾
	Q.rear = (Q.rear + 1) % MaxSize;		//队尾指针+1取余
	return true;
}

2.2.3 出队操作(删除)

//出队
bool DeQueue(SqQueue& Q, int& x) {
	if (Q.rear == Q.front)		//判断队空
		return false;
	x = Q.data[Q.front];		//x保存出队元素
	Q.front = (Q.front + 1) % MaxSize;	//队头指针后移
	return true;
}

        每出队一个元素,队头指针就向后移一位,同样用取模运算,让队头指针可以循环移动。

2.2.4 获取队头元素的值

//获得队头元素的值
bool GetHead(SqQueue& Q, int& x) {
	if (Q.rear == Q.front)		//判断队空
		return false;
	x = Q.data[Q.front];		//x保存出队元素
	return true;
}

2.2.5 计算队列元素个数

        (rear+MaxSize-front)%MaxSize

2.3 判断队空/队满

2.3.1 指针判别法

        在 2.2.2入队操作 一节已经详细讲过了,会浪费一个存储空间。如果自己写代码的话这种方法就够用了,但是考试过程中出题老师可能会要求你不让浪费掉这部分空间,于是有了后两种方法。

2.3.2 添加size计数器

//定义一个顺序队列
typedef struct {
	int data[MaxSize];		//静态数组存放队列元素
	int front, rear;		//队头指针和队尾指针
    int size;
}SqQueue;

        在初始化队列的时候,添加一个 size 用来记录队列中元素个数,插入成功 size++ ,出队成功 size-- ,当 size==MaxSize 时,队列已满,当 size==0 时,队列已空。初始化 size 为 0 。

2.3.3 添加入队/出队标志 tag

//定义一个顺序队列
typedef struct {
	int data[MaxSize];		//静态数组存放队列元素
	int front, rear;		//队头指针和队尾指针
    int tag;				//标志最近进行的是入队还是出队操作
}SqQueue;

        在定义一个队列的时候,添加一个tag标志,当执行入队操作时,令tag=1;执行出队操作时,令tag=0。我们知道,只有入队才会让队列已满,只有出队才会让队列已空,所以:

        队满条件:rear == front && tag == 1;

        队空条件:rear == front && tag == 0;

2.4 其他出题方法

        考试时可能会让队尾指针 rear 指向队尾元素,而不是队尾元素的下一位置,这样的话对应的判空、判满、入队、出队等操作的实现方式都要做一定的小小的改变。

2.5 链队列

2.5.1 链队列的声明

//声明一个结点
typedef struct LinkNode {	//链式队列结点
	int data;
	struct LinkNode* next;
}LinkNode;
//声明一个队列
typedef struct {
	LinkNode* front, * rear;	//队列的队头和队尾
}LinkQueue;

        带头结点和不带头结点两种实现方式的头指针 front 的指向是不一样的。

2.5.2 链队列的初始化

//初始化(带头结点)
void InitQueue(LinkQueue& Q) {
	//初始时,front、rear都指向头结点
	Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));	//申请内存空间
	Q.front->next = NULL;			//头结点的指针域置为空
}
//初始化(不带头结点)
void InitQueue(LinkQueue& Q) {
	Q.front = NULL;
	Q.rear = NULL;
}

2.5.3 链队列的入队

//入队(带头结点)
void EnQueue(LinkQueue& Q, int x) {
	LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));	//申请一个节点s
	s->data = x;
	s->next = NULL;
	Q.rear->next = s;		//新结点插入到rear之后
	Q.rear = s;				//尾指针指向s
}
//入队(不带头结点)
void EnQueue(LinkQueue& Q, int x) {
	LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));	//申请一个节点s
	s->data = x;
	s->next = NULL;
	if (Q.front == NULL) {			//当队列为空时,插入第一个元素
		Q.front = s;			//修改队头指针
		Q.rear = s;
	}
	else {
		Q.rear->next = s;		//新结点插入到rear之后
		Q.rear = s;				//尾指针指向s
	}
}

        代码不难,结合 2.5.1 链队列的声明 理解。注意插入的元素为第一个元素的情况。

2.5.4 链栈的出队

//出队(带头结点)
bool DeQueue(LinkQueue& Q, int& x) {
	if (Q.front == Q.rear)		//判空操作
		return false;
	LinkNode* p = (LinkNode*)malloc(sizeof(LinkNode));
	p = Q.front->next;		//p指向要删除的元素
	x = p->data;			//x保存要删除的元素的值
	Q.front->next = p->next;	
	if (Q.rear == p)		//如果要删除的是最后一个元素,要做特殊处理,即将队列置为空
		Q.rear = Q.front;
	free(p);
	return true;
}
//出队(不带头结点)
bool DeQueue(LinkQueue& Q, int& x) {
	if (Q.front == NULL)		//判空操作
		return false;
	LinkNode* p = (LinkNode*)malloc(sizeof(LinkNode));
	p = Q.front;		//p指向要删除的元素
	x = p->data;			//x保存要删除的元素的值
	Q.front = p->next;		//移动front指针的位置到下一结点
	if (Q.rear == p) {		//如果要删除的是最后一个元素,要做特殊处理,即将队列置为空
		Q.rear = NULL;
		Q.front = NULL;
	}
	free(p);
	return true;
}

        整体思路是用一个指针p指向被删节点,用x保存p的值,然后删除p结点即可。注意当删除元素为最后一个元素时,要做特殊处理,即把队列置为空。

2.6 双端队列

        双端队列是只允许从两端插入、两端删除的线性表。在此基础上也会延伸出“输入受限的双端队列、输出受限的双端队列”等概念,即只允许从一端插入、两端删除 或 两端插入、一端删除的操作。

考点:

  • 和栈相似,会让我们判断一个输入序列的输出序列是否合法,以及有多少种输出序列等。题目比较简单。

        注意:在栈中合法的输出序列,在双端队列中必定合法。

三、栈的应用

3.1 括号匹配(重要)

3.1.1 为什么用栈?

        比如“(((())))”一共8个括号,包含4个左括号和4个右括号。在匹配时最先出现的右括号匹配最后出现的左括号,即当出现一个右括号时,消除最后出现的左括号。这就和进栈出栈类似,也是后进先出。

3.1.2 算法思路

        最下面的蓝色框是结束框。

3.1.3 代码

//括号判断
bool bracketCheck(char str[], int length) {
	SqStack S;		//初始化一个栈
	InitStack(S);
	for (int i = 0; i < length; i++) {
		if (str[i] == '(' || str[i] == '[' || str[i] = '{') {
			Push(S, str[i]);		//扫描到左括号,入栈
		}
		else {
			if (StackEmpty(S))		//扫描到右括号且当前栈空
				return false;		//匹配失败
			char topElem;			//用来保存出栈的元素,即右括号
			Pop(S, topElem);		//栈顶元素出栈
			//判断括号是否匹配,若不匹配,返回false
			if (str[i] == '(' && topElem != ')')
				return false;
			if (str[i] == '[' && topElem != ']')
				return false;
			if (str[i] == '{' && topElem != '}')
				return false;
		}
	}
	return StackEmpty(S);		//检索完毕后若栈空,则匹配成功
}

 

        考试中可以直接使用基本操作,不过要用注释说明。本题采用顺序栈存储的实现方式,在实际应用过程中可能会出现栈满的情况,故可用链栈实现。但考试时用顺序栈就行,相对简单。

        代码思路:依次遍历所有元素,遇到左括号就入栈,遇到右括号就出栈;匹配失败的情况是左括号、右括号单独出现或左右括号不匹配。

3.2 表达式求值

        表达式共分为三种:中缀表达式,前缀表达式,后缀表达式。表达式分为三部分:操作数、运算符、界限符,比较通俗易懂,不做解释。

        其中,中缀表达式就是我们最熟悉的那种表达式,比如“((15÷(7-(1+1)))×3)-(2+(1+1))”,就是一个中缀表达式。考试时最长考的是后缀表达式,因为其应用面较广。

3.2.1 背景

        就是有个人,叫波兰,有一天突发奇想发明出一种不用界限符(即括号)也能无歧义地表达运算顺序的方法,就是现在的前缀表达式(又叫波兰表达式)和后缀表达式(又叫逆波兰表达式)。

3.2.2 三种表达式的对比和转化

3.2.2.1 对比
概念例子1例子2例子3
中缀表达式(最熟悉)运算符在两个操作数中间a+ba+b-ca+b-c*d
后缀表达式(最常考)运算符在两个操作数后面ab+ab+c-ab+cd*
前缀表达式运算符在两个操作数前面+ab-+abc-+ab*cd
3.2.2.2 中缀转后缀(手算)

        中缀转后缀的方式:先确定运算顺序,然后根据运算顺序依次按照“左操作数 右操作数 运算符”的顺序组合成一个新的操作数。

        注意:在实际操作过程中,我们会发现,一个中缀表达式的运算顺序可能并不是唯一的,比如 a+b-c 可以先算加法也可以先算减法,这就势必会导致转换出来的后缀表达式不唯一,理论上来说多种结果都正确,但由于算法三大特性之唯一性,我们规定“左优先”原则,即 只要左边的运算符能先计算,就优先算左边的 。这样可以保证结果唯一。

        比如下图两种计算方法,同样的式子,计算顺序不一样,转换出来的后缀表达式也不一样,即使两种结果都应该正确,但考试过程中以及计算机计算过程中都采用左边的那个,即采用左优先原则。

        同时,也要掌握后缀转中缀的计算方法。

3.2.2.3 后缀表达式计算(机算)

        后缀表达式的计算顺序和运算符的顺序相同。也就是让运算符前面最近的两个操作数进行运算。特点:最后出现的操作数先运算,也就是后进先出。这就和栈不谋而合。

用栈实现后缀表达式的计算:

  1. 从左往右扫描下一个元素,直到处理完所有元素;

  2. 若扫描到操作数则压入栈,并回到①;否则执行③;

  3. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压入栈顶,回到①。(注意,先弹出的是右操作数,后弹出的是左操作数)

3.2.2.4 中缀转前缀(手算)

计算方法:

  1. 确定中缀表达式中各个运算符的运算顺序;

  2. 选择下一个运算符,按照【运算符 左操作数 右操作数】的方式组合成新的操作数;

  3. 如果还有运算符未处理,就继续第二步。

右优先原则:只要右边的运算符能先计算,就优先计算右边的。

3.2.2.5 前缀表达式计算(机算)

        其实和后缀表达式的计算方法相似,只是要从右往左扫描了,并且弹出的顺序也有所不同,先弹出左操作数,后弹出右操作数

  1. 从右往左扫描下一个元素,直到处理完所有元素;

  2. 若扫描到操作数则压入栈,并回到①;否则执行③;

  3. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压入栈顶,回到①。(注意,先弹出的是左操作数,后弹出的是右操作数)

3.2.2.6 中缀转后缀(机算)

算法目的:给计算机一个中缀表达式,输出一个后缀表达式。

考点:考察进行到某一步时,栈内的情况是怎么样的,选择题。

学习目标:能用笔算的方式模拟整个过程,不需要会写代码。

过程:

  1. 初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:

    1. 遇到操作数。直接加入后缀表达式;

    2. 遇到界限符。遇到 “(” 直接入栈,遇到 “)” 则依次弹出栈内运算符并加入后缀表达式,直到弹出“)”为止。注意:“(”不加入后缀表达式;

    3. 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止,之后再把当前运算符入栈。

  2. 最后将栈中剩余运算符依次弹出,并加入后缀表达式。

        下图为手推过程,黑笔是栈,红笔是当前扫描的位置,蓝笔是当前后缀表达式。需要注意的是第⑧步中,“-”没有直接出栈是因为暂时不能确定它的运算顺序,比如D后面如果跟个“*”就要先计算后面的,所以要把“-”压入栈。每一步都根据上面说的步骤一一对应找一下理解理解就好了。

 

3.2.2.7 中缀表达式计算(机算)

        就是 后缀表达式计算中缀转后缀 的两种算法的结合。

过程:

  1. 初始化两个栈,一个操作数栈,用于保存不能确定运算次序的操作数;一个运算符栈,用于保存不能确定运算次序的运算符。

  2. 若扫描到操作数,压入操作数栈;

  3. 若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)

中缀转后缀的逻辑:

  1. 初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:

    1. 遇到操作数。直接加入后缀表达式;

    2. 遇到界限符。遇到 “(” 直接入栈,遇到 “)” 则依次弹出栈内运算符并加入后缀表达式,直到弹出“)”为止。注意:“(”不加入后缀表达式;

    3. 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止,之后再把当前运算符入栈。

  2. 最后将栈中剩余运算符依次弹出,并加入后缀表达式。

3.3 递归

        函数调用时需要用一个“函数调用栈”存储:调用返回地址、实参、局部变量。

        递归调用时,函数调用栈可称为“递归工作栈”。每进入一层递归,就将递归调用所需信息压入栈顶;每退出一层递归,就从栈顶弹出相应信息。

缺点:

  • 太多层递归可能会导致栈溢出。

  • 通常情况下,针对同一问题的递归算法求解比非递归算法求解的效率更低,因为递归算法包含很多重复运算,效率会降低。

四、数组和特殊矩阵

        数组的存储结构分为:一维数组和二维数组。之前只是泛泛了解一下,现在需要了解他们背后的实现细节。

        特殊矩阵分为:对称矩阵、三角矩阵、三对角矩阵、系数矩阵。主要是用一些简单的策略节省存储空间。

4.1 数组

4.1.1 一维数组

int a[10];

        在 c 语言中我们使用如上代码定义一个 int 类型的数组,各元素大小相同,且物理上连续存放。每一个元素所占的内存空间大小是一样的,都是4个字节(int型)。所以只要知道了一个数组的起始地址 LOC ,就可以计算出每一个元素 a[i] 的存放地址:LOC+i*sizeof(int) 。除非题目特别说明,否则默认数组下标从 0 开始。

4.1.2 二维数组

int a[2][4];

        使用如上代码定义一个二维数组。二维数组在逻辑上是两行,但是在内存中是线性存储的,如图:

        计算机会采用行优先存储或列优先存储来存储二维数组,本质上是为了把二维数组拉成线性结构,因为计算机内存是线性的。

        而线性存储二维数组所带来的好处就是可以实现随机存取。即如果知道二维数组的起始地址,那么只要给出某元素的行号和列号,就可以计算出该元素在内存中的存放位置。

        在M行N列的二维数组b[M] [N] 中,若按行优先,则bpi[j]的存储地址 = LOC+(i* N+j)*sizeof(int)。列优先类似。

4.2 矩阵

        普通矩阵可以采用二维数组的方式存储。注意:描述矩阵元素时,行列号通常从 1 开始;而描述数组时通常下标从0开始。这里不探讨普通矩阵,主要看看一些特殊矩阵是如何压缩存储的。

4.2.1 对称矩阵

        定义:若在 n 阶方阵中任意一个元素 ai,j 都有 ai,j=aj,i,则该矩阵为对称矩阵。

 

        普通存储:n*n二维数组;

        压缩存储:只存储主对角线+下(上)三角区。

4.2.1.1 按行优先存储

        要思考两个问题:

1.数组大小应该是多少?

        我们要存储下三角区,第一行 1 个元素,第二行 2 个元素,第三行 3 个元素......那么一共有 n 行,即一共有 (1+2+3+...+n)=(1+n)*n/2 个元素,即一维数组的长度应为(1+n)*n/2

2.如何将矩阵的行号和列号映射为对应数组的下标?

        按行优先的原则,ai,j 是第 [1+2+3+...+(i-1)]+j = i*(i-1)/2+j-1 个元素,注意数组下标若从 0 开始,下标要减一。那如果要访问上三角区(即j>i)的元素怎么办呢?由于是对称矩阵,所以 ai,j=aj,i,当访问上三角区的元素时,可以访问第 “ j*(j-1)/2+i-1 ”个元素(即i和j互换)。

        上述结论不用记,因为考试肯定不会考原题,需要会现推。考试可能会给你一维数组存储上三角区,或者按列优先原则存储,让求 ai,j 的下标,关键在于求出 ai,j 是第几个元素

4.2.2 三角矩阵

        定义:除了主对角线和下三角区,其他元素都相同。

        压缩存储:按行优先原则将橙色区域元素存入一维数组中。并在最有一个位置存储常量c。

4.2.2.1 下三角矩阵

        两个问题:

1.数组大小?

        n*(n+1)/2+1 。多出来的1是用来存放常量c的。

2.如何访问 ai,j ?

        k表示是第几个元素。

4.2.2.2 上三角矩阵

 

        数组大小和下三角矩阵一样。

        访问上三角区时,ai,j 元素的位置:n+(n-1)+...+(n-i+2)+j-i 。即ai,j 是第 (i-1)*(2 *n-i+2)/2+j-i 个元素。

4.2.3 三对角矩阵

        定义:三对角矩阵又称为带状矩阵,当 |i-j|>1 时,有ai,j=0 (1≤i,j≤n)。

        压缩存储:按行优先(或列优先)原则,只存储带状部分。

        两个问题:

1.数组大小?

        除第一行和最后一行是两个元素外,其余均为3个元素,故数组大小为 3n-2。

2.如何访问ai,j?

        按行优先原则,计算 ai,j 是第几个元素。

        前 i-1 行共有 3*(i-1)-1 个元素,ai,j 是第 i 行第 j-i+2 个元素,所以 ai,j 是第 2i+j-2 个元素。

Q:若已知数组下标k,如何得到i,j?

A:数组下标为 k ,即第 k+1 个元素。
前 i-1 行共 3(i-1)-1 个元素,显然,3(i-1)-1 < k+1 ≤ 3i-1       【即k+1处在两行之间】
解得 i ≥ (k+2)/3 ,向上取整即可求得 i 值
由 k=2i+j-3 得 j=k-2i+3

4.2.4 稀疏矩阵

        定义:非零元素远远少于矩阵元素的个数。

        特点:稀疏矩阵的特点是 矩阵中非零元的元素较少 ,切记不是“矩阵中的元素较少”!极容易看错!

压缩存储:

1.顺序存储——三元组<行,列,值>。

        上图稀疏矩阵的三元组如下:

 

        可以定义一个struct,有i,j,v三个属性,一个struct对应三元组中的一行,然后再定义一个struct的一维数组就可以顺序存储这些三元组。

        这种方式存储的话,如果要访问一个三元组,就需要顺序依次访问,就失去了随机存取的特性

考点:若采用三元组表存储结构存储稀疏矩阵M,则除了三元组表外,还需要保存M的行数和列数。
原因:每个非零元素都是由三元组组成,但是仅通过三元组表中的元素无法判断稀疏矩阵 M 的大小,因此还要保存 M 的行数和列数。此外,还可以保存 M 的非零元素个数。

2.链式存储——十字链表法  

        每个结点由三个数据域和两个指针组成,三个数据分别是存储元素的行号、列号、值,两个指针分别指向同一列的下一个元素、同一行的下一个元素。

4.3 考点

1.矩阵的压缩存储需要多长的数组?

2.由矩阵行列号 <i,j> 推出对应的数组下标号 k。

3.由 k 推出 <i,j> 。

4.易错点:

  • 存储上三角还是下三角;

  • 行优先还是列优先;

  • 矩阵元素从0开始还是从1开始;

  • 数组下标从0开始还是从1开始。

五、习题精选

        本章节代码题不多,主要是栈的基本操作那一部分,并且一般不用写出基本操作的函数实现,只要会用就行(包括入栈、出栈、判空、栈满、入队、出队等操作)。以下这些都是王道25考研数据结构书的课后习题里面我的错题,以及一些比较有代表意义的题目,不多,但应该会很有帮助

1.设链表不带头结点且所有操作均在表头进行,则下列最不适合作为链栈的是:          (C)

A.只有表头结点指针,没有表尾指针的双向循环链表

B.只有表尾结点指针,没有表头指针的双向循环链表

C.只有表头结点指针,没有表尾指针的单向循环链表

D.只有表尾结点指针,没有表头指针的单向循环链表

         解析:对于双向循环链表,不管是表头指针还是表尾指针,都可以很方便地找到表头结点,方便在表头做插入或删除操作。而单循环链表通过尾指针可以很方便的找到表头结点,但通过头指针找尾节点需要遍历一次链表,故选C。

2.一个栈的入栈序列为1,2,3,...,n,出站序列是P1,P2,...Pn。若P2=3,则P3可能取值的个数为:        (n-1)

        解析:P3可以取除了3之外的所有数。自己推演一遍就会了。

3.与顺序队列相比,链式队列    (D)

A.优点是队列的长度不受限制

B.优点是进队和出队时间效率更高

C.缺点是不能进行顺序访问

D.缺点是不能根据队首指针和队尾指针计算队列长度

        解析:A选项,链式队列的长度虽然不固定,但仍不能无限扩容,因此仍然受限制。B选项,顺序队列和链式队列进出队的时间复杂度都是O(1)。C选项,顺序队列和链式队列都可以顺序访问。D选项,对于顺序队列,可以通过队头指针和队尾指针计算队列中的元素个数,而链式队列则不能。

4.用链式存储方式的队列进行删除操作时需要                                                    (D)
A.仅修改头指针                      B.仅修改尾指针
C.头尾指针都要修改                     D.头尾指针可能都要修改 

        解析:仅针对AD选项,队列中只有一个元素时需要修改头尾指针,要使队列为空,其余情况仅修改头指针即可。

5.若将n阶下三角矩阵A按列优先顺序压缩存放在一维数组B[1...n(n+1)/2+1]中,则存放到B[k]中的非零元素ai,j(1≤i,j≤n)的下标i、j与k的对应关系是:                        

                                                                                                (    (j-1)(2n-j+2)/2+i-j+1    ) 

        解析:题干关键信息:下三角,n阶,列优先。

        那么元素 ai,j 之前共有 j-1 列,即有 n+(n-1)+(n-2)+...+(n-j+2)=(j-1)(2n-j+2)/2 个元素,元素 ai,j 是第 j 列上第 i-j+1 个元素,数组 B 的下标从 1 开始,k=(j-1)(2n-j+2)/2+i-j+1。

 6.有一个n×n的对称矩阵A,将其下三角部分按行存放在一维数组B中,而 A[0] [0]存放于B[0]中,则元素 A[i] [i]存放于B中的哪个下标处?                                     (        (i+3)*i/2        )

        解析:先计算 A[i] [i]是第几个元素,再计算它的下标。

        由题意可知, 由于对称矩阵A的元素是从第 0 行 0 列开始存储,所以A[i] [i] 是第 i+1 行第 i+1 列的那个元素。并且是下三角部分按行存储,第一行1个元素,第二行2个元素,...,第 i 行 i 个元素,因此前 i 行有 (1+2+...+i)=(i+1)*i/2 个元素,然后再加上第 i+1 行的 i+1 个元素,则 A[i] [i] 是第 (i+1) *i/2+i+1 个元素。

        由于数组B的下标从0开始,故 A[i] [i] 的存放位置的下标要减一。即元素A[i] [i] 存放的下标为 (i+1) *i/2+i =(i+3) *i/2

        其他的还有很多给出元素下标让计算数组下标的题目,平时刷题过程中多注意练习就好,不算难。

  • 29
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
西南交通大学的考研数据结构和C语言真题主要涵盖了数据结构和C语言的基本概念、常见算法和数据结构的应用,是考研复习中的重点和难点。 数据结构部分的真题主要涉及线性表、栈和队列、链表和树、图和排序等知识点。例如,可能会出现关于数组的插入、删除和查找操作以及对其时间复杂度的分析题目,还可能会要求设计和实现单链表、二叉树或图等数据结构,并进行相应的操作和应用。对于这些题目,考生需要熟悉各种数据结构的特点、使用方法和算法,能够分析算法的时间复杂度和空间复杂度,并灵活应用到实际问题中。 C语言部分的真题主要考察C语言的基本语法、指针和内存管理、函数和库等方面的知识。可能会出现关于函数的声明和定义、指针的使用、内存动态分配和释放等方面的题目。考生需要对C语言的语法、特性和常用库函数有一定的掌握,能够理解和分析C语言程序的执行过程和内存管理机制。 对于准备西南交通大学考研的考生来说,要复习数据结构和C语言,首先要掌握基础概念和常用算法和数据结构的原理和应用。其次,要多做真题和模拟题,加深对知识的理解和应用。同时,还要关注最新的考研动态和备考资料,及时调整和完善复习计划。通过系统的学习和不断的练习,相信考生一定能够顺利应对西南交通大学考研数据结构和C语言的考试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值