链结构
1 #include<stdio.h>
2
3 /*
4 数据结构:
5 在大一上学期,我们学过了C语言程序设计,可以说,所有C语言的语法规则都讲清楚了,如果学好了,给你一段C语言的
6 代码,至少可以看懂了。而下学期要学的,很重要的一门课:数据结构,则是在C语言的基础上,灵活应用,来实现一些功能
7 或者使得代码更加简洁。
8 数据结构最主要的框架就是C程序设计中的结构体,我们来讲解一下结构体,结构体就是通过将不同的变量封装在一起,
9 用来描述一个特定的事物,目的是使得代码更加条理清楚,比如,我们要描述一个空间中的点,我们可以用,x、y、z来表示
10 三维坐标,那么,使用时,我们要定义int x,y,z;如果是数组,我们还需要int x[10],y[10],z[10],然后使用时一一对应,
11 x[1]、y[1]、z[1]描述的是第一个点的三维坐标,这样子,代码就会显得很零散,如果这个事物的属性更多时,我们便难以判
12 断使用,所以,我们可以采用结构体来定义,比如三维空间的点可以定义成:
13 typedef struct point //typedef 起到的是重新给point这个结构体起名字的作用。
14 {
15 int x,y,z; //将x、y、z封装在一起,共同来描述点这个事物
16 }Point; //这样子我们便可以用Point a; 来申请一个点所需的空间,用起来更加的方便。
17 而数据结构里就讲解了一些前人定义好的,一些常见的结构体,通过这些结构体,我们可以更加方便地编程。
18
19
20 链结构:
21 定义:数据存储或逻辑上是链式的
22 基本实现方式:数组和链表
23
24 ==============================================================================================================
25 数组:
26 数组大家都接触得比较多,就不细讲了,不过在结构化中,我们可以将数组的大小和数组封装在一起,如下:
27 typedef struct
28 {
29 int data[1000];
30 int num;
31 }Node;
32 这样子,我们便可以用Node a;定义一个数组,并且a.num是这个数组中存放数的数量。
33 ==============================================================================================================
34 链表:
35 我们可以通过指针或者数组下标将一些元素链接在一起,和数组相似又不完全一样。
36 一. 指针链接的链表:
37 指针初步了解:
38 我们先来了解一下指针,比如说int *a; 我定义了一个指向整型数据的指针变量a,我们可以用这个指针指向另外一个整型
39 变量,比如说int b=3; a=&b;我们把b的地址给a,然后我们便可以通过a来访问b所在的空间来查看或修改b这个变量空间里面的
40 内容,比如说printf("%d\n",*a);我们可以输出b空间里的内容,比如*a=4;我们修改b这个变量的值为4;
41 基本上,指针有两种用法,一个比较简单,就像是前面那样,我们将这个指针指向某一个已经定义好的变量来访问这个变
42 量,或者,我们为这个指针申请一个空间,a=(int *)malloc(sizeof(int)); 这段代码就叫做动态申请,我们通过这段代码手
43 动地向系统申请到了一个整型数据大小的空间,现在,这个指针就是通往这个空间的梯子,通过这个指针我们可以访问到这个
44 空间,当然,如果我们现在将指针a指向b,也是可以的,但你申请到的这个空间就再也找不到了,因为你把梯子给撤掉了,这
45 个空间又没有别的标记,自然就再也找不到了,这就是所谓的内存泄漏,我们写程序的时候要避免出现这种情况。
46 链表实现:
47 指针的用途基本看到了,我们现在用指针来链接几个元素,来构建一个链表。
48 我们可以封装一个结构体如下:
49 typedef struct node
50 {
51 int data; //这个链表所存放的元素,这里用int来定义,也可以是自己定义的其他结构体。
52 struct node *next; //这是一个struct node类型的指针,用来指向下一个这种类型的变量
53 }Node;
54 现在我们先定义两个变量:Node a,b;
55 接下来,我们赋值:a.data=1; b.data=2; 然后要将这两个变量链接在一起:
56 a.next=&b; //将b的地址赋值给a的next属性,这样,a.next就指向了b,自然,a、b就连在一起了,如果b的后面没有别的
57 元素,我们可以让b的next指向NULL,表明b是这个链表的最后一个元素:b.next=NULL;
58 这样子,一个最基本的链表就构建完成了,表头是a,表尾是b,甚至,我们接下来都用不到b这个变量名了,a.next就表示
59 b。不管链表的元素有多少,我们都可以用表头来表示任意一个元素,差别只不过是.next的数量而已,比如有五个元素,那么
60 a.next->next->next->next就代表着第五个元素。当然,实际应用的时候,肯定不是这样用的。
61 我们平时写链表的时候都是动态申请空间的,也就是说链表里面的每一个元素都是通过malloc来获取空间的,如下:
62 Node *CreatNode() //写一个单独函数来重复调用,每一次调用获得一个Node变量大小的空间
63 {
64 Node *s;
65 s=(Node *)malloc(sizeof(Node));
66 s->next=NULL; //如果后面不接东西的话,必须置为NULL,这样遍历链表的时候遇到NULL就表示链表结束
67 return s; //创建完这个空间,要将这个空间的地址返回回去
68 }
69 接下来,要创建链表,我们可以先调用一次CreatNode来获取表头。
70 Node *p; //我们设立的表头p
71 p=CreatNode(); //一般链表有两种模式:有表头和无表头,有表头的意思就是在正式数据前多弄一个空的变量,这样做
72 //的好处,用过就知道!
73 比如现在我们不断添加元素,从键盘读取整数,遇到-9999时链表创建结束,如下:
74 while(1)
75 {
76 scanf("%d",&x); //输入
77 if(x==-9999) break; //如果是-9999就退出循环
78 Node *s;
79 s=CreatNode(); //定义了一个指针,用过不指向一个可用的空间的话,被称作是野指针,直接用程序会崩的
80 s->data=x; //赋值
81 s->next=p->next; //这种插入方式叫做头插法,也就是说每创建一个变量就插在表头的后面
82 p->next=s; //将表头的next指针指向s,那么p->next就等于s,这就是将这些元素链接在一起的链。
83 }
84 这样子,链表就创建完毕。那么如何通过一个表头来遍历整个链表呢。如下:
85 比如我要输出这个链表:
86 void Dis(Node *p) //将你要输出的链表的表头当作参数传递给函数
87 {
88 p=p->next; //表头是空的
89 while(p) //当p不是NULL的时候,说明p->data是有值得,当等于NULL的时候,就是链表的结尾了
90 {
91 printf("-> %d ",p->data); //输出
92 p=p->next; //p移动到他链接的下一个变量上。
93 }
94 printf("\n");
95 //指针和函数参数深入:
96 //不知道大家会不会疑惑,我没有新生成一个指针来遍历数组,直接用表头来遍历,会不会出现遍历一遍,链表就找不
97 //到了,因为老师肯定讲过swap(int *a,int *b)的例子,当传递的是指针进来的时候,好像可以实现a、b的互换,为
98 //什么这里可以直接用表头遍历呢。其实和函数的形参一个道理,函数在接收外面传进来的参数的时候,其实是自己重
99 //新弄了一个变量来接收,所以才会出现函数内部a、b交换了而外部不变的现象,用指针之所以可以交换是因为,虽然
100 //指针也是新创的,但由于值一样,所以指向的空间一样,通过地址来改变里面的元素,所以可以实现交换,但是如果
101 //函数内部是直接改变指针的值,也就是指针指向的地址的话,他改变的只是函数内部的这个变量值,外面的实参是不
102 //会有变化的。
103 }
104 遍历链表的方法很重要,很多操作都是通过这个方法来实现的。
105 比如说,我要所有data值为5的结点,如下:
106 void Del(Node *p,int x) //表头和删除的值传进来
107 {
108 Node *q; //重新弄一个指针来遍历链表
109 q=p->next; //表头里面是不存值的
110 while(q) //当q不为空,也就是链表还没结束的时候
111 {
112 if(q->data==x) //如果q指向的结点的data值等于x,那么就要删除它
113 {
114 p->next=q->next; //p一直是q的前一项,现在删除q就是将p的next指针指向q的下一项,自然就跳过了q
115 free(q); //因为空间是我们动态申请的,现在也要手动收回,不然会内存泄漏
116 q=p->next; //q重新指向p的下一项
117 }
118 else
119 {
120 p=p->next; //如果不等于x,p就跳一格,q也跳一格,始终保持q在p的下一项
121 q=q->next;
122 }
123 }
124 }
125 插入与删除类似。
126 值得注意的是:严谨的代码中,动态申请到的空间,必须在不用时释放掉,不然会造成内存泄漏,如下:
127 void Des(Node *p) //递归程序
128 {
129 if(p) //如果还没到达链表尾部,那么就要先释放掉该结点后面的结点,再释放本结点
130 {
131 Des(p->next);
132 free(p);
133 }
134 }
135 以上就是用指针链接的链表,建议大家都自己手写一份,实现插入、删除等操作。。。
136 现在大家有没有发现数组和链表的优缺点,明明数组更加简便,为什么要创造链表这种东西呢?
137 实际上,对于插入、删除操作来说,链表比起数组是有巨大优势的,数组想要插入一个数,插入以后还得移动n个数来维持
138 顺序,但对于链表,你只需要找到这个数的位置,然后修改上一个结点的指针指向就可以了。不需要移动大量的数据,如果这
139 个表非常长的话,那么优势是巨大的。当然,如果你想知道某一个位置的数的话,那么数组可以直接输出,链表却需要O(n)
140 的时间,因此,数组和链表是各有优缺点的。
141 二. 数组下标链接的链表:
142 比如我们有一串数3 4 7
143 那么我们可以构成一个数组表
144 地址下标 存放的数 下一个数存放的位置,如果为-1就代表这个数就是表尾
145 0 | 3 | 1
146 1 | 4 | 2
147 2 | 7 | -1
148 我们可以用一个二维数组来实现这个功能,比如:a[100][2]; 总共有100个空间,a[i][0]代表存放的数、a[i][1]代表下
149 一个数存在的位置,这样子,我们便构建了一个链表,只不过这个链表不是由指针链接,而是由下标链接在一起的。那么,他
150 和前面讲的链表一样,实现方法也差不多,你们可以自己手写一份。
151 链表的用法多种多样,而且可以根据实际用途来自己决定他的结构,比如我们可以将最后一个结点的next指针指向第一个
152 结点,形成循环链表,比如我们可以在每个结点内多包含一个struct node *pre; 用来指向上一个结点,称作双向链表。还可
153 结合到栈和队列中去用,总之,只要能实现目标,一起都好说。。。。
154
155 注:
156 我们来看一下有表头的链表和没有表头的链表的差别:(指针实现)
157 如果有表头,删除第一个结点的时候,直接将表头的next指向第二个结点就可以了,如果没有表头,你的头指针指向了第
158 二个结点,但你必须将这个头指针返回回去,或者通过双重指针来实现,不然就会出错,原因和上面的形参实参关系一样。
159 */