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

news2024/10/5 16:21:41

一、栈

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1578147.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript - 你是如何区分一个变量是对象还是数组的

难度级别:中高级及以上 提问概率:65% 我们日常如果想要获得一个变量的类型,大多会使用typeof的方法,但typeof却不是很准确,遇到null、数组或是对象这种数据类型的时候,他就失灵了,返回值是object,那么都有哪些方式可以区分一个变量的类…

【CHI】(十二)Memory Tagging

目录 1. Introduction 2. Message extensions 3. Tag coherency 4. Read transaction rules 4.1 TagOp values 4.2 Permitted initial MTE tag states 5. Write transactions 5.1 Permitted TagOp values 5.2 TagOp, TU, and tags relationship 6. Dataless transact…

机器学习笔记 - 深度学习遇到超大图像怎么办?使用 xT 对极大图像进行建模论文简读

作为计算机视觉研究人员,在处理大图像时,避免不了受到硬件的限制,毕竟大图像已经不再罕见,手机的相机和绕地球运行的卫星上的相机可以拍摄如此超大的照片,遇到超大图像的时候,我们当前最好的模型和硬件都会达到极限。 所以通常我们在处理大图像时会做出两个次优选择之一:…

并发编程三大特性之可见性

一、什么是可见性&#xff1f; 可见性问题是基于CPU位置出现的&#xff0c;cpu处里速度非常快&#xff0c;相对CPU来说去主内存 获取数据这个事情太慢了&#xff0c;CPU就提供了 L1&#xff0c;L2&#xff0c;L3的三季缓存&#xff0c;每次去主内存拿完 数据后&#xff0c;数据…

SIT1051AQ5V 供电,IO 口兼容 3.3V,±58V 总线耐压,CAN FD 静音模式总线收发器

SIT1051AQ 是一款应用于 CAN 协议控制器和物理总线之间的接口芯片&#xff0c;可应用于车载、工业 控制等领域&#xff0c;支持 5Mbps 灵活数据速率 CAN FD &#xff0c;具有在总线与 CAN 协议控制器之间进行差分信 号传输的能力。 SIT1051AQ 为 SIT1051Q 芯片的…

python应用-计算两个日期的时间差

学习目录 1. 安装deteutil包 2. 导入relativedelta类 3. 计算两个日期的差值 4. 计算1个日期和时间差相加后得到新的日期 之前在工作中遇到一个使用场景&#xff1a;需要计算两个日期之前的差值&#xff0c;比如相差了几年几月几日&#xff0c;查找资料发现deteutil包的rel…

基于Leaflet.js的Marker闪烁特效的实现-模拟预警

目录 前言 一、闪烁组件 1、关于leaflet-icon-pulse 2、 使用leaflet-icon-pulse 3、方法及参数简介 二、闪烁实例开发 1、创建网页 2、Marker闪烁设置 3、实际效果 三、总结 前言 在一些地质灾害或者应急情况当中&#xff0c;或者热门预测当中。我们需要基于时空位置来…

行云防水堡-打造企业数据安全新防线

企业数据安全&#xff0c;顾名思义就是通过各种手段或者技术或者工具保障企业数据的安全性&#xff1b;保障数据信息的硬件、软件及数据受到保护&#xff0c;不受偶然的或者恶意的原因而遭到破坏、更改、泄露&#xff0c;系统连续可靠正常地运行&#xff0c;信息服务不中断。目…

[C++][算法基础]合并集合(并查集)

一共有 n 个数&#xff0c;编号是 1∼n&#xff0c;最开始每个数各自在一个集合中。 现在要进行 m 个操作&#xff0c;操作共有两种&#xff1a; M a b&#xff0c;将编号为 a 和 b 的两个数所在的集合合并&#xff0c;如果两个数已经在同一个集合中&#xff0c;则忽略这个操…

数据库讲解---(SQL语句--表的使用)【MySQL版本】

零.前言 数据库讲解&#xff08;MySQL版&#xff09;&#xff08;超详细&#xff09;【第一章】-CSDN博客 数据库-ER图教程_e-r图数据库-CSDN博客 数据库讲解&#xff08;MySQL版&#xff09;&#xff08;超详细&#xff09;【第二章】【上】-CSDN博客 一.SQL概述 1.1SQL简…

FaceForensics++数据库下载(一步步解析过程)

FaceForensics数据库下载&#xff08;超详细版教程&#xff09; 相信很多做deepfake相关研究的朋友&#xff0c;在对模型进行测试或者对潜前人的研究进行复现时&#xff0c;都需要下载一系列数据库并进行预处理等操作&#xff0c;而FaceForensics数据库是一个由数千个使用不同…

地又接错了?又冒烟了吧?

原文来自微信公众号&#xff1a;工程师看海&#xff0c;与我联系&#xff1a;chunhou0820 看海原创视频教程&#xff1a;《运放秘籍》 大家好&#xff0c;我是工程师看海&#xff0c;原创文章欢迎点赞分享&#xff01; 作为一名硬件工程师&#xff0c;理解地的概念是至关重要的…

大数据之搭建Hive组件

声明&#xff1a;所有软件自行下载&#xff0c;并存放到统一目录中 1.Hive组件的安装配置 1.1实验环境 服务器集群3 个以上节点&#xff0c;节点间网络互通&#xff0c;各节点最低配置&#xff1a;双核 CPU、8GB 内存、100G 硬盘运行环境CentOS 7.4服务和组件完成前面章节实验…

redis的简单操作

redis中string的操作 安装 下载可视化软件&#xff1a;https://gitee.com/qishibo/AnotherRedisDesktopManager/releases。 Mac安装redis&#xff1a; brew install redisWindows安装redis: 安装包下载地址&#xff1a;https://github.com/tporadowski/redis/releases 1.…

内存管理new and delete(C++)

在本篇中&#xff0c;将会较为详细的介绍在 Cpp 中的两个新操作符 new 和 delete&#xff0c;将会介绍其中的底层原理&#xff0c;以及这两个操作符的使用方法。其中还介绍了 new/delete 操作符使用的细节&#xff0c;还扩展了一些有关定位 new 表达式的知识点。最后总结了 mal…

C++练级之路——类和对象(上)

1、类的定义 class 类名{//成员函数 //成员变量}; class为定义的关键字&#xff0c;{ }内是类的主体&#xff0c;注意后面的 ; 不要忘了 类体中的内容成为类的成员&#xff0c;类中的变量为成员变量或类的属性&#xff0c;类中的函数为成员函数或类的方法&#xff0c; 类的两种…

Prompt最佳实践|大模型也喜欢角色扮演?

在OpenAI的官方文档中已经提供了Prompt Enginerring的最佳实践&#xff0c;目的就是帮助用户更好的使用ChatGPT 编写优秀的提示词我一共总结了9个分类&#xff0c;本文讲解第2个分类&#xff1a;要求模型扮演角色 提供更多的细节要求模型扮演角色使用分隔符指定任务步骤提供样…

OPC UA遇见chatGPT

最近opc 基金会将召开一个会议&#xff0c;主题是”OPC UA meets IT“。由此可见&#xff0c;工业自动化行业也开始研究和评估chatGPT带来的影响了。 本文谈谈本人对OPC UA 与chatGPT结合的初步实验和思考。 构建OPC UA 信息模型 chatGPT 的确非常强大了&#xff0c;使用自然…

前端开发之el-table(vue2中)固定列fixed滚动条被固定列盖住

固定列fixed滚动条被固定列盖住 效果图前言解决方案 效果图 前言 在使用fixed固定列的时候会出现滚动条被盖住的情况 解决方案 改变el-table固定列的计算高度即可 .el-table {.el-table__fixed-right,.el-table__fixed {height:auto !important;bottom:15px !important;}}

安装cuda后只在root用户下可见,非root不可见问题

0. 安装cuda和nvidia driver步骤可以参考这篇&#xff1a; https://blog.csdn.net/mygugu/article/details/137474101?spm1001.2014.3001.5502 1.问题记录&#xff1a; 这里记录下安装cuda后遇到的一个奇葩问题&#xff0c;因为安装过程需要root权限&#xff0c;安装后发现…