数据结构精录&总结Episode.5 数据结构入门之数组与广义表详解(基于Visual C++)

这是本周唯一的一个周末。

也不知道当前全世界的疫情情况如何了,一直宅在家,偶尔看看新闻,基本上也没有心思关注全世界的疫情发展新动向了。而且刚刚了解到财经大学的通知,我们的在家线上学习会持续到九月份开学。这也就意味着短短的2019-2020学年在经过了三个月的在校学习之后,很快就要告一段落了。

本来大学也不是深入玩本领的时代,我们更多地需要去了解去喜爱那些专业知识,这样长时间离校的生活看起来也就没有那么突兀了。

博主曾经经历过长时间不在班集体学习的生活,参加高考前八个月才回到自己的班集体和同学们一起上课。如今突然感觉正是有了那段日子,如今每天在家学习也就不那么感到无聊了。

和全国各地的专业/业余程序爱好者们共勉~~


数组和广义表的知识非常繁多,而且设计C语言和C++比较核心的内容,因而以下只简要总结了其中有关数据结构的内容,部分总结参考@WhiteBlackCat和@xiaoliangliang的总结文章,在此表示感谢。

广义表的相关总结

一.定义

广义表是线性表的推广,广义表中每个元素可以是原子,也可以是子表,原子即单个元素,而子表是广义表。我们可以发现,其实python中的数组就是一个广义表,其内元素可以是单个的元素,也可以是一个数组。

二.广义表的长度和深度

1.长度

广义表的长度就是看第一层所含的元素个数

2.深度

广义表的深度是max(每个元素深度) + 1

  • A=():A是一个空表,长度为0,深度为1
  • B=(e):B只有一个原子e,B的长度为1,深度为1
  • C=(a,(b,c,d)):C的长度为2,深度为2
  • D=(A,B,C)=((),(e),(a,(b,c,d))):D的长度为3,深度为3
  • E=(a,E):E是一个递归的表,长度为2,深度无限。

三.广义表的存储结构

1.头尾链表存储表示

a):首先,我们要知道一个概念,当广义表LS非空时,我们把第一个元素a1称为LS的表头,其余元素加上最外层的括号组成的表(a2,a3,a4,...,an)作为LS的表尾。我们可以发现,表头可能是原子也可能是表,但是表尾一定是表。

b):我们可以发现广义表中有两种不同的元素,所以我们可以定义两个不同的结点,表结点和原子结点来存储。

表结点,其中hp指向表头,tp指向表尾。

原子结点

2.子表存储表示

a):这种表示方法应该说更好理解一些,就是把表中每个元素分开来看,而不是从表头表尾去分析。

表结点,其中hp指向子表,tp指向下一个元素结点。

表结点,其中data是原子项的值,tp指向下一个元素结点。

 

对于广义表而言,利用递归求深度是最简洁的算法之一:

int GListDepth(GList L){
	if(!L) return 1;//空表则返回1
	if(L->tag == 0) return 0;//原子项返回0
	for(max = 0, pp = L; pp; pp = pp->ptr.tp){//在元素间循环遍历
		dep = GListDepth(pp->ptr.hp);//获取每个子表深度
		if(dep > max) max = dep;//和最大的比较
	}
	return max + 1;//由于深度是每个元素深度+1.所以返回max+1
}

数组的相关总结

数组是C语言和指针有着不清不楚的关系,涉及到指针那数组就变得有点邪恶了,来说下数组

数组的定义

类型 数组名[元素个数]
例如
int a[6] //整型储存元素为6的a数组,下面依次类推
char b[24]
double c[3]

数组的初始化

将数组中所有元素初始化为0,可以在这么写:
int a[10]={0}//事实上这里只是讲第一个元素赋值为0,未赋值的会自动化赋值为0
如果是赋予不同的值,那么用逗号分隔开即可
int a[10]={1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
你还可以只给一部分元素赋值,未被赋值的元素自动初始化为0
int a[10]={1, 2, 3, 4, 5, 6};//表示为前面6个元素赋值,后面4个元素系统自动初始化为0
有时候还可以偷懒,可以只给出各个元素的值,而不指定数组的长度(因为编译器会根据值的个数自动判断数组的长度):
int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0,};
C99

增加了一种新特性:指定初始化的元素。这样就可以只对数组中的某些指定元素进行初始化赋值,而未被赋值的元素自动化初始为0:
int a[10] = {[3] = 3, [5] = 5, [8] = 8};

访问数组中的元素

数组名[下标]
列:a[0];//访问a数组中的第一个元素
b[1];//访问b数组中的第二个元素
c[5];//访问c数组中的第六个元素
注意:
int a[5];//创建一个具有五个元素的数组
a [0];//访问第一个元素的下标是0,不是1
a [5];//报错,因为第五个元素的下标是a[4]

 

关于数组和广义表的详细知识及算法实例,依照惯例以代码形式展现如下,详解详析已写入注释中:

// 第五章 数组与广义表.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。编写—JoeyBG,算法尚有不足之处,敬请谅解。
//

#include <iostream>
#include<algorithm>
#include<math.h>
#include<iomanip>
using namespace std;


// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单

// 入门使用技巧: 
//   1. 使用解决方案资源管理器窗口添加/管理文件
//   2. 使用团队资源管理器窗口连接到源代码管理
//   3. 使用输出窗口查看生成输出和其他消息
//   4. 使用错误列表窗口查看错误
//   5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
//   6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件


/*
数组与广义表基本知识点总结

//数组存储方式
1、从本质上讲,数组与顺序表、链表、栈和队列一样,都用来存储具有 "一对一" 逻辑关系数据的线性存储结构。只因各编程语言都默认将数组作为基本数据类型,使初学者对数组有了 "只是基本数据类型,不是存储结构" 的误解。
2、不仅如此,数组和其他线性存储结构不同,顺序表、链表、栈和队列存储的都是不可再分的数据元素(如数字 5、字符 'a' 等),而数组既可以用来存储不可再分的数据元素,也可以用来存储像顺序表、链表这样的数据结构。
3、比如说,数组可以直接存储多个顺序表。我们知道,顺序表的底层实现还是数组,因此等价于数组中继续存储数组。这与平时使用的二维数组类似。
P.S.无论数组的维数是多少,数组中的数据类型都必须一致。

//多维数组存储查找算法
1、当需要在顺序存储的多维数组中查找某个指定元素时,需知道以下信息:
多维数组的存储方式;
多维数组在内存中存放的起始地址;
该指定元素在原多维数组的坐标(比如说,二维数组中是通过行标和列标来表明数据元素的具体位置的);
数组中数组的具体类型,即数组中单个数据元素所占内存的大小,通常用字母 L 表示;
2、根据存储方式的不同,查找目标元素的方式也不同。如果二维数组采用以行序为主的方式,则在二维数组 anm 中查找 aij 存放位置的公式为:
LOC(i,j) = LOC(0,0) + (i*m + j) * L;
其中,LOC(i,j) 为 aij 在内存中的地址,LOC(0,0) 为二维数组在内存中存放的起始位置(也就是 a00 的位置)。
3、而如果采用以列存储的方式,在 anm 中查找 aij 的方式为:
LOC(i,j) = LOC(0,0) + (i*n + j) * L;

//非满矩阵的三大压缩存储方式
1、对称矩阵的压缩存储实现过程是:若存储下三角中的元素,只需将各元素所在的行标 i 和列标 j 代入下面的公式:
k=i(i-1)/2+j-1
存储上三角的元素要将各元素的行标 i 和列标 j 代入另一个公式:
k=j(j-1)/2+i-1
最终求得的 k 值即为该元素存储到数组中的位置(矩阵中元素的行标和列标都从 1 开始)。
2、上三角/下三角阵:对于这类特殊的矩阵,压缩存储的方式是:上(下)三角矩阵采用对称矩阵的方式存储上(下)三角的数据(元素 0 不用存储)。
3、压缩存储稀疏矩阵的方法是:只存储矩阵中的非 0 元素,与前面的存储方法不同,稀疏矩阵非 0 元素的存储需同时存储该元素所在矩阵中的行标和列标。

//广义表!!!必考知识点(毙考题)
1、定义:广义表是线性表的推广,也称为列表。
同数组类似,广义表中既可以存储不可再分的元素,也可以存储广义表,记作:
LS = (a1,a2,…,an)
其中,LS 代表广义表的名称,an 表示广义表存储的数据。广义表中每个 ai 既可以代表单个元素,也可以代表另一个广义表。
原子和子表
通常,广义表中存储的单个元素称为 "原子",而存储的广义表称为 "子表"。
例如创建一个广义表 LS = {1,{1,2,3}},我们可以这样解释此广义表的构成:广义表 LS 存储了一个原子 1 和子表 {1,2,3}。
以下是广义表存储数据的一些常用形式:
A = ():A 表示一个广义表,只不过表是空的。
B = (e):广义表 B 中只有一个原子 e。
C = (a,(b,c,d)) :广义表 C 中有两个元素,原子 a 和子表 (b,c,d)。
D = (A,B,C):广义表 D 中存有 3 个子表,分别是A、B和C。这种表示方式等同于 D = ((),(e),(b,c,d)) 。
E = (a,E):广义表 E 中有两个元素,原子 a 和它本身。这是一个递归广义表,等同于:E = (a,(a,(a,…)))。
注意,A = () 和 A = (()) 是不一样的。前者是空表,而后者是包含一个子表的广义表,只不过这个子表是空表。
2、广义表的常见操作包含取表头和取表尾工作:
当广义表不是空表时,称第一个数据(原子或子表)为"表头",即GetHead(L),剩下的数据构成的新广义表为"表尾",即GetTail(L)。
P.S.重点易错!除非广义表为空表,否则广义表一定具有表头和表尾,且广义表的表尾一定是一个广义表。
例如在广义表中 LS={1,{1,2,3},5} 中,表头为原子 1,表尾为子表 {1,2,3} 和原子 5 构成的广义表,即 {{1,2,3},5}。
*/


//广义表基础的一个常见应用:数字矩阵
typedef struct
{
	int x;
	int y;
} Position;//数字矩阵位置存储的结构体定义,包含横纵坐标

int m[30][30];//地图创建
Position here = { 0,0 }, nextt = { 0,0 };//当前位置,下一个位置创建
Position DIR[4] = { 0, 1, 1, 0, 0, -1, -1, 0 };//右下左上方向数组

void Init(int n)
{
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= n; j++) //方格阵列初始化为0
			m[i][j] = 0;
	}
	for (int j = 0; j <= n + 1; j++) //方格阵列上下围墙
		m[0][j] = m[n + 1][j] = -1;
	for (int i = 0; i <= n + 1; i++) //方格阵列左右围墙
		m[i][0] = m[i][n + 1] = -1;
}//初始化数字矩阵的操作函数

void Print(int start, int endi)//start, endi为开始和结束下标
{
	for (int i = start; i <= endi; i++)
	{
		cout << m[i][start];
		for (int j = start + 1; j <= endi; j++)
			cout << "\t" << m[i][j];
		cout << endl;
	}
	cout << endl;
}//输出打印函数

// n:原问题规模
// m:地图矩阵
void Solve(int n)
{
	here.x = 1;//左上角有蛋糕的位置
	here.y = 1;
	int dirIndex = 0;
	int num = 1;
	m[1][1] = 1;
	while (num < n * n)
	{
		nextt.x = here.x + DIR[dirIndex].x;
		nextt.y = here.y + DIR[dirIndex].y;
		if (m[nextt.x][nextt.y] == 0) //判断下一个位置是否有蛋糕
		{
			m[nextt.x][nextt.y] = ++num; //吃了蛋糕,拉出的数字加1
			here = nextt;   //以next为当前位置,继续走
		}
		else
			dirIndex = (dirIndex + 1) % 4;//换下一个方向,按右下左上顺序继续吃蛋糕
	}
}//问题解决的算法核心函数


int main()
{
	char judge = '\0';
	cout << "数据结构精录&总结 Episode.5 数组与广义表—编写&调试:JoeyBG,算法尚有不足之处,敬请见谅" << endl;
	cout << "--------------------------------------------------------------------------------------------" << endl;
	cout << endl;//引言框
startlabel:
	int n = 0;
	cout << "请输入大于等于1且小于等于20的整数n:";
	cin >> n;
	while (n < 1 || n>20)
	{
		cout << "输入数据不符要求,请重新输入大于1小于等于20的整数n:";
		cin >> n;
	}//判断n的输入是否符合我们程序的编写能力
	cout << endl;
	Init(n);
	Print(0, n + 1);
	Solve(n);
	Print(1, n);//各操作函数执行区段
	cout << "是否继续进行输入计算或退出程序?(Y/N):";
judgelabel:
	cin >> judge;
	if (judge == 'Y' || judge == 'y')
	{
		system("cls");
		goto startlabel;
	}
	else if (judge == 'N' || judge == 'n')
	{
		exit(0);
	}
	else
	{
		cout << "输入有误,请重新输入:";
		goto judgelabel;
	}//判断是否需要继续程序的进行,如果是就清屏重新开始主程序,否则重新输入或退出
	return 0;
}//主函数接口


/*
参考资料:
1、解学武:数组和广义表,http://data.biancheng.net/array_list/
2、陈小玉:趣学数据结构,人民邮电出版社,2019.09
*/

别的都是了解熟悉即可,但广义表的两大操作方式则是历年来考研、期末、面试的重中之重,不得不再强调一次:

※当广义表不是空表时,称第一个数据(原子或子表)为"表头",即GetHead(L),剩下的数据构成的新广义表为"表尾",即GetTail(L)。
P.S.除非广义表为空表,否则广义表一定具有表头和表尾,且广义表的表尾一定是一个广义表。

【例如在广义表中 LS={1,{1,2,3},5} 中,表头为原子 1,表尾为子表 {1,2,3} 和原子 5 构成的广义表,即 {{1,2,3},5}。】

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值