C 语言中指针的常规操作

前言

虽然在算法竞赛中很少用到指针,因为指针一般都有其它结构可以代替,并且指针很容易出错且难以发觉;不过在考研中似乎指针是绕不开的,并且在底层设计中 C 语言的指针确实有着速度优势,因此本文将整理有关指针的常规操作,包括指针的基本用法、利用指针实现链表、树与图中的指针等,不照顾毫无 C 基础的。

既然考研中用 C 和 CPP 答题都可以,那本文就用 C++ 语法。

指针的声明与赋值

指针的声明方法

这应该是 C 语言基础知识中的,下面四个指针声明语句都是正确的:

	int* p1;
	int*p2;
	int *p3;
	int * p4;

可见空格对指针的声明没有影响。但是如果想一次声明多个指针就要注意了:

	int* p1,p2;//错误,该语句定义了一个 int 类型指针 p1 和 int 变量 p2
	int *p3, *p4;//正确,定义了两个int类型指针 p3 和 p4

即对于每个指针类型的变量,都需要一个 * 。
在 C++ 中,int* 是一种复合类型,是指向 int 类型的指针。

以上语法都是针对声明指向 int 类型的指针,指向其它类型(如:double、char、float、甚至自定义的结构体 Node)都是类似,只需要把变量类型 int 改为对应的变量类型即可。

对指针进行赋值

在对指针进行赋值之前,首先要明白指针中存放的是什么。
指针指针,故名思意就是用来指向某个地方的,在 C 语言中,指针存放的是地址,所谓的地址即变量在内存中的地址,通过这个地址可以在内存中找到该位置,然后对该位置上的数据进行操作。
既然如此,要对指针赋值,就要赋给指针一个同样数据类型的地址,提到地址又不得不提及取地址符(&):

	int a = 10;
	int *p = &a;//此时 p 中存放的是 a 的地址

取地址符(&)故名思意就是把某个变量的地址取出来,他的用法如上述代码所述。
但是下述对指针赋值的代码是错误的:

	int a = 10;
	int *p;
	*p = &a;//错误,正确写法是 p = &a; 

也就是说 p 才是变量名,它是一个指针变量,所以加上 * 干什么。

读取指针上的值

这里又分为两种情况,一种是基本数据类型(int、double等)的指针变量,另一种是结构体或类(不常用)类型的指针变量。
因为指针变量中存放的其实是地址,因此对于基本数据类型的指针,想要读取该地址上的值可以在指针前加上 * 来访问该位置的值:

	int a = 10;
	int *p = &a;
	printf("%d\n",*p); //输出 10

特殊照顾结构体:

但是如果访问一个结构体类型的指针就会遇到一个问题,我们是把一个结构体变量看作一个整体,一个指针指向的是这个整体的地址,并不是细化到指向其中每一个变量。这个时候如果再用 * 取出整体的内容毫无意义,于是对于结构体我们定义了新的操作:

struct Node{
	int a,b,c;
}node; 
int main(){
	node = Node{1,2,3};
	Node* p = &node;
	printf("%d %d %d\n",p->a,p->b,p->c);
	return 0;
}

即利用node->a 来读取 node 结构体中的名字为 a 的成员变量。

使用指针的小建议
  1. 在定义指针的时候最好给它先赋值 NULL ,不然它的内容是随机的,一旦这个时候向该地址读取或写入内容,结果也是随机的(轻则答案出错,重则程序崩溃)。
  2. 如何申请空间搜其它资料把,下文都会用 new 语法,不用 malloc 。
  3. 在释放指针时一定要确保其指向了合适恰当的内容。

链表

下面介绍用 C++ 语言实现链表数据结构。在此不介绍链表的背景、优缺点等,直接说构造方式。
链表的构造不同,它们的功能(优缺点)也不同,我们也给它们起了不同的名字,常见的有:单向链表、双向链表、循环链表等。这些链表实现的基本思路相同,只有细微的区别而易。 同样的,即使是同一种链表,也有不同的实现方式,结构一般也会有细微区别,比如是否有头节点、尾节点等。
看了一下考研教材,大多代码都是默认有头节点,因此本文就以有头节点的链表为例。由于我是写给我自己复习的,所以为了节省时间,基础知识不整理,只整理关键部分。

注:本部分代码大部分改自浙大数据结构网课

单向链表

也叫单链表。即只能向一个方向遍历,一般我们都用结构体来实现单链表。
从需求出发,对于链表上的每一个节点,我们需要存放数据的地方(不存数据要链表干什么?),为了使得各个节点能串在一起,我还需要知道下一个节点在哪(没必要知道上一个节点在哪,不然还配叫单链表吗)。我们是用指针来存放下一个节点的地址。

如果每个节点按照上述需求构造,那么每个节点应该有一个数据域,一个指针域。数据域用来存放数据,指针域用来存放指针。具体的,单链表节点的结构体应具有如下形式:

typedef struct LNode *PtrToLNode;
struct LNode {
    ElementType Data; //数据域
    PtrToLNode Next; //指针域
};

其中 ElementType 是自定义的数据类型,可以替换为自己需要的类型。PtrToLNode 是一个指针类型,其指向 LNode 数据类型。上面第一行代码可以查阅其它资料理解。

注:如果上述代码单独不好理解可以参照 附录A 整体代码

插入操作
有了节点之后,下一个问题就是如何将多个节点组织在一起。
显然我们需要一个入口才能成功访问一个链表,于是我们需要一个头节点来记录入口。
如果我们的需求只是将一个新的元素插入到链表末尾,那么每次只需要从头节点出发,找到最后一个节点并插入即可:

/* 带头结点的插入 */
bool Insert( List L, ElementType X){ 
	/* 这里默认L有头结点 */
    Position tmp, pre;
 
    /* 查找P的前一个结点 */        
    for ( pre=L; pre->Next; pre=pre->Next ) ;            
    /* 找到了最后一个节点 */
    tmp = (Position)malloc(sizeof(struct LNode)); /* 申请、填装结点 */
    tmp->Data = X; 
    tmp->Next = NULL;   /* 一定要赋值 */
    pre->Next = tmp;
    return true;
}

实际上在试卷上写的代码应该不用这么复杂正式,但是整理时还是规范点好。

删除操作
删除操作一般应该是重点,因为不同的赋值顺序会导致结果的不同,因此在选择题中很喜欢考。其实只需要记住,我在删除的这条链接之后还会不会用到?比如有单链表上的三个节点:a—>b—>c,我要删除 b,那我不能直接断开第一条链,因为那样就找不到 c 了,正确的做法是直接用第二条链替换第一条,即: a->Next = b->Next;

/* 带头结点的删除 */
/*注意:这里P是拟删除结点指针 */
bool Delete( List L, Position P ){ 
	/* 这里默认L有头结点 */
    Position tmp, pre;
 
    /* 查找P的前一个结点 */        
    for ( pre=L; pre&&pre->Next!=P; pre=pre->Next ) ;            
    if ( pre == NULL || P == NULL) { /* P所指的结点不在L中 */
        printf("删除位置参数错误\n");
        return false;
    }
    else { /* 找到了P的前一个结点pre */
        /* 将P位置的结点删除 */
        pre->Next = P->Next;
        free(P);	//释放 P 的空间 
        return true;
    }
}

单链表还是比较简单的,但是双链表、循环链表也不过是在单链表的基础上设计的而易。

如果想要判断单链表中当前节点是否为尾节点,只要用pre->Next == NULL 判断即可。

双向链表

单链表只能从头节点向尾节点遍历,如果我们在访问某个节点时不仅仅需要访问它的儿子,还可能访问它的父亲,在单链表中要想实现“访问父亲节点”操作只能从头再来一次,因为单链表是顺序访问的,指针域没有记录“回去的路”;此时,为了支持回退操作,我们在指针域增设一个指向当前节点父亲的指针,即 Par :

typedef int ElementType;
typedef struct LNode *PtrToLNode;
struct LNode {
    ElementType Data;
    PtrToLNode Next , Par;
	/*指向下一个(儿子)节点,上一个(父亲)节点*/ 
};

应当注意的是,有的双向链表(甚至单链表)是有尾指针的,所谓的尾指针即一个指向尾节点的指针,用于快速检索到尾节点(类似于头指针);在题目中(尤其是选择题)如果没有提及,一般是不含尾指针的。

此部分节点的添加删除比单链表复杂一点点,应该关注一下,但我没整理。

循环链表

前面介绍的两种链表都是只能从一端向另一端遍历,一旦走到末尾就停止了。如果我们定义尾节点的下一个是头节点,头节点的前一个是尾节点,那我们遍历的时候就可以不停的转圈圈,想转几圈就转几圈。其可以用于类似于约瑟夫环等问题。

基于前面的单链表和双链表,显然循环链表也可以有两种实现方式,一种是只能单向循环,另一种是可以双向循环。
无论是单向循环还是双向循环,都只需要把尾指针的 Next 从 NULL 改为 head->Next (头节点)即可;双链表要再让头节点指向尾节点节点。(不是头指针和尾指针,别忘了头指针和尾指针不存放数据)

这里的删除和插入操作与前面俩个链表类似,只是在处理首尾位置时可能略有不同,可以参考一下代码并结合其它资料理解。这个不给代码了。

关于链表的建议

不同教材实现链表的方式都有细微的区别,其实不应该纠结于一个统一规范的模板,我们应该关注的是该结构能实现什么样的功能,怎么实现。
就比如在循环链表中,有的书将头指针和尾指针也包含在循环中,但是由于这两个指针只起到索引作用,不存放数据,所以放进去也要结合实际需求,如果一旦进入循环就不关心转了几圈,那这两个指针放进去也没啥用。

由于链表以及其它的数据结构都没有像数学定义一样明确的规定,都是可以很灵活的实现的,所以有时需要根据题意推测它到底是哪种实现方式(有没有头指针、尾指针)。

另外就是关于判断一个链表是否走到末尾:

  • 对于单链表,如果 pre->Next = NULL,则 pre 就是最后一个节点。
  • 对于双向链表,如果 pre->Next = NULL ,则 pre 就是最后一个节点;若 pre->par = head,则 pre 就是第一个节点。
  • 对于循环链表,如果 pre->Next = head->Next ,则 pre 就是最后一个节点。

选择题中常考的是如何在单(双)链表某个位置插入(删除)一个新节点,这种题最好的做法是画个图,然后看看需要连几个边,断几个链。

未完待续

附录

附录 A

单向链表增删查,在末尾添加元素。

#include<bits/stdc++.h>
using namespace std;
typedef int ElementType;
typedef struct LNode *PtrToLNode;
struct LNode {
    ElementType Data;
    PtrToLNode Next;
};
typedef PtrToLNode Position;
typedef PtrToLNode List;
 
/* 查找 */
#define ERROR NULL
 
Position Find( List L, ElementType X ){
    Position p = L; /* p指向L的第1个结点 */
 
    while ( p && p->Data!=X )
        p = p->Next;
 
    /* 下列语句可以用 return p; 替换 */
    if ( p ) return p;
    else return ERROR;
}
 
/* 带头结点的插入 */
bool Insert( List L, ElementType X){ 
	/* 这里默认L有头结点 */
    Position tmp, pre;
 
    /* 查找P的前一个结点 */        
    for ( pre=L; pre->Next; pre=pre->Next ) ;            
    /* 找到了最后一个节点 */
   // tmp = (Position)malloc(sizeof(struct LNode)); /* 申请、填装结点 */
   	tmp = new LNode; 
    tmp->Data = X; 
    tmp->Next = NULL;
    pre->Next = tmp;
    return true;
}
 
/* 带头结点的删除 */
/*注意:这里P是拟删除结点指针 */
bool Delete( List L, Position P ){ 
	/* 这里默认L有头结点 */
    Position tmp, pre;
 
    /* 查找P的前一个结点 */        
    for ( pre=L; pre&&pre->Next!=P; pre=pre->Next ) ;            
    if ( pre == NULL || P == NULL) { /* P所指的结点不在L中 */
        printf("删除位置参数错误\n");
        return false;
    }
    else { /* 找到了P的前一个结点pre */
        /* 将P位置的结点删除 */
        pre->Next = P->Next;
        free(P);	//释放 P 的空间 
        return true;
    }
}

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:上身试试 返回首页