数据结构杂谈
在学习数据结构时,绝大多数人都能看懂算法,但是上手写代码却是一头雾水,这个问题在我刚开始学习数据结构的时候给我带来了很大的困扰,差点就放弃了,总结下来就是对于一些开头的定义和结构体定义,以及指针的使用不熟悉。本文将会详细讲解上述三个问题。
定义
- #define
利用 #define 将一个标识符定义为一个字符串
例如
#define PI 3.14159265
就是我们最常见的宏定义。代表PI就是3.141559265。 - const
const 在实际编程中用得并不多,const 是 constant 的缩写,意思是“恒定不变的”!它是定义只读变量的关键字,或者说 const 是定义常变量的关键字。说const定义的是变量,但是又相当于常量,说他定义的是常量又有变量的属性,所以叫常变量。
用const定义的变量的值是不允许改变的,常用来表示最大长度,或者某个重复使用的量。
const int MAX = 255
- 定义数据类型
为了程序在修改数据类型的时候不必逐行更改,将会使用typedef对一个数据类型进行定义。
例如
typedef int ElementType
给int整形起了个别名叫做ELementType,需要更改数据类型的时候只需要更改int就可以了。 - typedef struct
相信很多人在数据结构学习当中发现结构体的定义都是这样的:
typedef struct ListNode { ElementType Element; struct ListNode *Next; }Node, *PNode;
并且在声明结构体变量的时候是
Node N
或者是PNode P
,难道不应该是struct ListNode L
,其实都是可以的,但是ListNode L
会报错,这是因为ListNode是相当于一个标识符,而不是一个结构体,只有struct 和 ListNode 合在一起才表示一个结构体类型。那么为什么Node N,PNode P
可以呢?这是因为typedef给这个结构体起了新的名字PNode以及Node。
指针
在指针方面我主要介绍指针的定义,&和*符号以及指针在函数中的应用。
- 指针的定义
int p; //这是一个普通的整型变量 int *p; //P是一个整型数据的指针 int p[3]; //P是一个由整型数据组成的数组 int *p[3]; //P是一个由返回整型数据的指针所组成的数组 int (*p)[3];//P是一个指向由整型数据组成的数组的指针 int **p; //指针所指向的元素是指针,该指针所指向的元素是整型数据,也称为二级指针,用的很少。
在定义一个指针的时候,从变量名处起,根据运算符优先级结合,其中仅代表变量是一个指针。** 指针的值是指针本身储存的数值,这个数值会被当作一个地址,指针所指向的内存去就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针类型)的一片内存。
- 指针的运算
在数据结构当中很少用到,简单介绍一下,对于指针也可以进行运算
int *p = a[0];p++
这样p会指向a[1],为什么不是地址的值+1呢,实际上在做指针运算时编译器会吧指针的值加上sizeof(指针类型),对于上述例子指针p实际上被加了4。 - 运算符&和*
&取地址运算符,*是间接运算符。
&a的运算结果是该变量的内存地址,也可以说是一个指针。
*p的运算结果就是p指向的东西。
int a = 5; int b; int *p; int *pp; p = &a; //p指向a,把a的地址赋值给p *p = 24;//p指向的本来是5,改成24,相当于变量a等于24,因为p所指向的和变量a是同一块内存地址 *pp = &p;//pp二级指针指向p。 **pp = 34;//把变量a,指针p所指向的内存地址存放的数据改为34。
这些就是指针的基本用法。
- 指针的应用
指针具体的应用更多的体现在结构体和函数上。
typedef struct ListNode { ElementType Element; struct ListNode *Next; }Node, *PNode;
其中PNode就是一个结构体指针,访问其内容的时候就可以用->对其成员进行访问,那为什么要用结构体指针而不是直接使用结构体呢?因为通常我们不会把一个数据结构的操作放在一起,而是以函数的形式分开操作,保证了程序的封装性和模块化,在使用函数时,参数往往是指针,因为指针指向的是地址,所以在函数内对结构体的更改就可以保存下来,如果是普通结构体,则只有一个返回值,并不会对整体的数据结构进行修改,具体实例我将会在之后的博客具体介绍。 关于指针在函数中的应用我将会在函数部分进行介绍。
函数
对于函数在数据结构中的使用,大多数的返回值都是void,因为往往需要一个函数对整个数据结构进行修改,并不需要返回值。我在这里具体说明关于函数参数和返回值的问题。 如果函数使用参数,则需要声明接收参数的变量,这些变量称为函数的形式参数,在函数退出时将会销毁。
当调用函数的时候有两种向函数传递参数的方式:
- 传值调用:该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
- 引用调用:通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。 在数据结构中往往都是引用调用,都是对实参本身进行操作,
函数的返回值可以是一种数据类型的值也可以是一个指针,返回值为指针大多数用于初始化数据结构时使用,如创建链表:
PNode CreateList(void) {
int len ; // 用于定义链表长度
int val ; // 用于存放节点数值
PNode PHead = (PNode)malloc(sizeof(Node)); // 创建分配一个头节点内存空间
//头节点相当于链表的哨兵,不存放数据,指向首节点(第一个节点)
if (PHead == NULL) // 判断是否分配成功
{
printf("空间分配失败 \n");
exit(-1);
}
PNode PTail = PHead; // 链表的末尾节点,初始指向头节点
PTail->Next = NULL; // 最后一个节点指针置为空
printf("请输入节点个数:");
scanf("%d", &len); // 输入节点个数
for (int i = 0; i < len; i++) {
PNode pNew = (PNode)malloc(sizeof(Node)); // 分配一个新节点
if (pNew == NULL) {
printf("分配新节点失败\n");
exit(-1);
}
printf("请输入第 %d 个节点的数据:", i + 1);
scanf("%d", &val); // 输入链表节点的数据
pNew->Element = val; // 把数据赋值给节点数据域
PTail->Next = pNew; // 末尾节点指针指向下一个新节点
pNew->Next = NULL; // 新节点指针指向为空
PTail = pNew; // 将新节点复制给末尾节点
}
printf("创建链表成功\n");
return PHead; // 返回头节点
}
最后返回的是链表的头节点PHead。其中需要重点解释的就是PNode PHead=(PNode)malloc(sizeof(Node));
这一段代码说明了为什么一开始在定义结构体时分别定义了PNode 和 Node,据我观察大多数是为了在给新建的节点指针分配内存空间。
在理解了上述内容的情况下,相信各位可以更好的学习数据结构,更快的上手实现数据结构以及各种算法。