指针与链表
指针与链表
各位CTFer可以忽略这篇文章~
各位CTFer可以忽略这篇文章~
各位CTFer可以忽略这篇文章~
指针
指针的定义
指针对于变量来讲就像单人间的宿舍号一样。每个人(变量)都会有唯一的宿舍(地址),宿舍号(指针)可以迅速帮我们定位每个人(变量)。而指针变量是专门用来存放某变量的地址的变量。
定义语法
1 | int *p1; |
要注意:
1 | int *p1, p2; |
这句话的意思是p1为一个指向int类型变量的指针变量,而p2是一个int类型的变量。如果希望p2也是指针,需要在p2前面也加上*。通常编辑器会将int和*连在一起,而*和p1分开,像:
1 | int* p1, p2; |
在写的时候注意一下就先行,反正我不喜欢这样。
使用语法
以下是对指针的赋值和调用的一些例子:
1 | int array[100]; |
*代表取某地址上的值,&代表取某变量的地址。
通过上述代码我们可以发现指针没什么卵用甚至很麻烦,让我们看一看它在函数中的用途:
1 | void func(int p) |
1 | void func(int *p) //将p指向传入的地址 |
通常来讲,如果不定义全局变量,一个函数是无法修改其他函数中的变量的。但是上面两段代码告诉了我们,如果使用指针我们便可以实现跨函数修改变量。
总的来说,如果*p是一个指针变量,那么p代表地址,*p代表该地址上的值。
再进一步
1 | int a = 0x1234; //0x代表16进制 |
对于上述代码我们发现同样是指针,仅仅是转换了指向对象的类型就会使得输出造成巨大的差别。这是为什么呢?
这是因为int类型的变量在内存中占据32位(称为32 bit或者4 byte)的空间,即转成二进制之后长度不能超过32,而char类型的变量只占据8位。因此,当我们将&a从int*转成char*后,只会保留后8位。再加上神奇的16进制数每一个数字占4位的特性,恰好就是后两位16进制数。
1 | int a[10] = {5, 4, 3, 2, 1}; |
通过上述代码,我们发现数组的名字就是数组首元素的地址;(数组名字+i)就是数组第i个元素的地址。
链表
链表的定义
链表顾名思义就是串成一条线的元素。这看起来跟数组很像,直接用数组不好吗?
事实上,链表的优势在对大量数据的增删改查。比如一个数组有1000万个元素,我要删掉中间那一个,那么就得把后500万个元素全都向前移一位。但链表仅需要单纯的删掉就可以了。
链表的结构
链表上的元素我们称之为“节点”,每个节点分为数据域和指针域
head | node | node | node | node | node | tail | |
---|---|---|---|---|---|---|---|
节点号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
数据域 | num | num | num | num | num | num | num |
指针域 | 1 | 2 | 3 | 4 | 5 | 6 | NULL |
我们可以发现节点的指针域都指向下一个元素,尾元素指向NULL。在代码实现过程中节点编号是不存在的,此处仅为了方便展示。
如果我们想在上述链表尾部添加一个节点,可以分成3步:
第1步:构造当前节点
head | node | node | node | node | node | tail | new | |
---|---|---|---|---|---|---|---|---|
节点号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
数据域 | num | num | num | num | num | num | num | num |
指针域 | 1 | 2 | 3 | 4 | 5 | 6 | NULL | NULL |
第2步:尾标识的指针指向当前节点
head | node | node | node | node | node | tail | new | |
---|---|---|---|---|---|---|---|---|
节点号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
数据域 | num | num | num | num | num | num | num | num |
指针域 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | NULL |
第3步:尾标识改为当前节点
head | node | node | node | node | node | node | tail | |
---|---|---|---|---|---|---|---|---|
节点号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
数据域 | num | num | num | num | num | num | num | num |
指针域 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | NULL |
如果我们想在上述链表中间添加一个节点,也可以分成3步:
第1步:构造当前节点
head | node | node | node | node | node | node | tail | new | |
---|---|---|---|---|---|---|---|---|---|
节点号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
数据域 | num | num | num | num | num | num | num | num | num |
指针域 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | NULL | NULL |
第2步:修改当前节点的指针
head | node | node | node | node | node | node | tail | new | |
---|---|---|---|---|---|---|---|---|---|
节点号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
数据域 | num | num | num | num | num | num | num | num | num |
指针域 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | NULL | 4 |
第3步:修改插入位置(3)的指针
head | node | node | node | node | node | node | tail | node | |
---|---|---|---|---|---|---|---|---|---|
节点号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
数据域 | num | num | num | num | num | num | num | num | num |
指针域 | 1 | 2 | 3 | 8 | 5 | 6 | 7 | NULL | 4 |
整理一下可以变成更好看的模样:
head | node | node | node | node | node | node | node | tail | |
---|---|---|---|---|---|---|---|---|---|
节点号 | 0 | 1 | 2 | 3 | 8 | 4 | 5 | 6 | 7 |
数据域 | num | num | num | num | num | num | num | num | num |
指针域 | 1 | 2 | 3 | 8 | 4 | 5 | 6 | 7 | NULL |
我们可以类似地进行增删改查等操作。
定义语法
1 | struct NODE |
其中data为数据域,*next为指针域。下面为先输入n再输入n个数,并构建一条链表的代码。
1 | int i, n, temp; |
对于上述代码,可以发现我们虽然创建了一条链表,但是它好像看不见摸不着,不像数组一样可以知道它的名字,访问具体元素等。的确,链表的访问只能通过从首节点开始遍历,但如果结合二叉树等数据结构,将会大大降低这个过程所花费的时间。
同时,构造链表的时候只能一个一个节点地申请内存,因为没人知道到底应当开多大。
链表的增删改查
基础样式如下:
以下代码中头节点算作第1个节点
1 | void add(NODE *tail, NODE *node) //在链表尾部插入新节点 |
基于上述代码我们可以发现对于链表的遍历都是以节点进行的。
小作业?
1.读代码写结果(不要让计算机帮你读哦):
1 |
|
2.读代码写结果(不要让计算机帮你读哦):
输入:
1 | 5 |
1 |
|
3.按要求写代码
构造一个函数,传入头节点head,目标编号x和新建节点node。使得新建节点插入在第x个节点后面。头节点视为第0个节点。