上一篇文章讲了,数据结构包括四大逻辑结构和两大物理结构,以下:node
集合结构没有太多的东西,之后须要的时候再提一下就好。咱们知道线性结构有数组、链表、队列等
,那么本篇文章就来针对链表进行讲解。数组
链表在逻辑上是线性的,可是在物理存储上倒是链式存储的。所以也就被称为线性表的链式存储。
从名字上看就能够知道,这家伙应该是像铁链同样,一环扣一环的,而事实上也正是如此。数据结构
话很少说,链表主要有如下四种:函数
节点是链表中具体存储数据的地方,能够当作是铁链上的环。
节点主要分为两块,数据域和指针域。oop
#import <Foundation/Foundation.h>
#define ERROR 0
#define SUCCESS 1
typedef int hyStatus;
// 定义 节点 结构体类型
typedef struct Node {
int data; // 数据域
struct Node *next; // 指针域
}Node;
// 定义 链表 类型
typedef Node* LinkList;
// 建立一个链表
LinkList InitLinkList(void);
// 头插法添加一个节点
hyStatus addNodeToHeader(LinkList *ll, int data);
// 尾插法添加一个节点
hyStatus addNodeToFooter(LinkList ll, int data);
// 指定位置插入一个节点
hyStatus addNodeToCustom(LinkList *ll, int data, int index);
// 删除指定位置的节点
hyStatus delNodeWithIndex(int index, LinkList *ll);
// 查找某个数据第一次出如今哪一个节点
int checkIndexInLinkList(LinkList ll, int data);
// 查找指定index处的节点的data值
int checkDataInLinkList(LinkList ll, int index);
// 遍历链表打印其中的data
void foreachLinkList(LinkList ll);
// 销毁一个节点变量
hyStatus freeNode(Node *node);
// 销毁一个链表
hyStatus freeLinkList(LinkList *ll);
复制代码
.m文件(为了方便就直接使用.m文件写了,反正OC彻底支持C语言)post
#import "LinkList.h"
// 建立一个节点
Node * InitNode(int data) {
Node *node = malloc(sizeof(Node));
if (!node) {
printf("建立Node失败\n");
return NULL;
}
node->data = data;
node->next = NULL;
return node;
}
// 头插法添加一个节点
hyStatus addToHeader(LinkList *ll, Node *node) {
node->next = *ll;
*ll = node;
return SUCCESS;
}
// 尾插法添加一个节点
hyStatus addToFooter(LinkList ll, Node *node) {
LinkList p = ll;
while (p->next) {
p = p->next;
}
p->next = node;
return SUCCESS;
}
// 指定位置插入一个节点
hyStatus addToCustom(LinkList *ll, Node *node, int index) {
if (index < 1) {
printf("index最小要为1,表示插入到第一个位置\n");
return ERROR;
}
// 若是要插入的位置是第一个
if (index == 1) {
node->next = *ll;
*ll = node;
}
else {
LinkList p = *ll;
// 要插入的位置是第2到最后,找到要插入位置的前一个位置
for (int i = 1; i < index - 1; i++) {
if (p->next) {
// 2----length - 1
p = p->next;
}
else {
// other
printf("要插入的位置比链表长度还大\n");
return ERROR;
}
}
node->next = p->next;
p->next = node;
}
return SUCCESS;
}
// 建立一个链表
LinkList InitLinkList() {
// 开辟一块能够存放一个Node类型变量的堆空间,而且把这块空间的地址返回
LinkList ll = malloc(sizeof(Node));
if (!ll) {
printf("开辟Node空间失败\n");
return NULL;
}
ll->data = 0;
ll->next = NULL;
return ll;
}
// 头插法添加一个节点
hyStatus addNodeToHeader(LinkList *ll, int data) {
Node *node = InitNode(data);
if (node) {
return addToHeader(ll, node);
}
return ERROR;
}
// 尾插法添加一个节点
hyStatus addNodeToFooter(LinkList ll, int data) {
Node *node = InitNode(data);
if (node) {
return addToFooter(ll, node);
}
return ERROR;
}
// 指定位置插入一个节点
hyStatus addNodeToCustom(LinkList *ll, int data, int index) {
Node *node = InitNode(data);
if (node) {
return addToCustom(ll, node, index);
}
return ERROR;
}
// 删除指定位置的节点
hyStatus delNodeWithIndex(int index, LinkList *ll) {
if (index < 1) {
printf("节点的最小位置是1, index < 1");
return ERROR;
}
Node *node;
if (index == 1) {
if ((*ll)->next) {
node = *ll;
*ll = (*ll)->next;
return freeNode(node);
}
else {
return freeNode(*ll);
}
}
else {
// 不改动第一个节点,则不须要使用*ll
/* node\*ll-->1-->2-->3 */
node = *ll;
Node *p = NULL;
for (int i = 1; i < index; i++) {
if (node->next) {
p = node;
node = node->next;
}
else {
printf("要删除的index比链表的长度还大\n");
return ERROR;
}
}
// 删除中间节点
p->next = node->next;
return freeNode(node);
}
}
// 查找某个数据第一次出如今哪一个节点
int checkDataInLinkList(LinkList ll, int data) {
int i = 1;
do {
if (ll->data == data) {
return i;
}
ll = ll->next;
i++;
} while (ll);
return ERROR;
}
int checkIndexInLinkList(LinkList ll, int index) {
if (index < 1) {
printf("index不能小于1\n");
return ERROR;
}
/* ll-->1-->2-->3-->4 */
for (int i = 1; i < index; i++) {
if (ll->next) {
ll = ll->next;
}
else {
printf("index不能大于链表的总长度\n");
return ERROR;
}
}
return ll->data;
}
// 遍历链表打印data
void foreachLinkList(LinkList ll) {
do {
printf("--------%d---------\n", ll->data);
ll = ll->next;
} while (ll);
}
// 销毁一个节点变量
hyStatus freeNode(Node *node) {
node->data = 0;
node->next = NULL;
free(node);
node = NULL;
return SUCCESS;
}
// 销毁一个链表
hyStatus freeLinkList(LinkList *ll) {
Node *node;
while ((*ll)->next) {
// node保存当前节点,*ll指向下一个节点
node = *ll;
*ll = (*ll)->next;
// 释放node节点
freeNode(node);
}
// 释放*ll中next为空的最后一个节点
return freeNode(*ll);
}
复制代码
调用,在main.m中调用的。ui
#import "LinkList.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 建立一个链表
LinkList ll = InitLinkList();
// 循环添加几个节点
for (int i = 1; i <= 10; i++) {
// 头插法
// addNodeToHeader(&ll, i);
// 尾插法
addNodeToFooter(ll, i);
}
// 遍历链表
foreachLinkList(ll);
// 任意插入
printf("=========================\n");
addNodeToCustom(&ll, 28, 12);
foreachLinkList(ll);
// 删除指定位置的节点
printf("++++++++++++++++++++++++++\n");
delNodeWithIndex(5, &ll);
foreachLinkList(ll);
// 查询节点
int index = checkDataInLinkList(ll, 4);
printf("查找到的位置是:%d\n", index);
int data = checkIndexInLinkList(ll, 5);
printf("查找到的数据是:%d\n", data);
}
return 0;
}
复制代码
注释写的很明确,应该就不须要过多的解释了。
须要注意的是,单向链表和单向循环链表都没有使用head
节点。spa
前面已经说了,单向循环链表就是在单向链表的基础上首尾相连。以下图: 指针
代码实现:
分文件写有点麻烦,就所有写到main.m文件中了。code
#import <Foundation/Foundation.h>
#define ERROR 0
#define SUCCESS 1
typedef int hyStatus;
// 定义 节点 结构体类型
typedef struct Node {
int data; // 数据域
struct Node *next; // 指针域
}Node;
// 定义 链表 类型
typedef Node* LinkList;
// 建立循环链表
LinkList createLoopLinkList(void) {
LinkList ll = NULL; // 始终指向链表头
LinkList r = NULL; // 始终指向链表尾
int data;
printf("输入非负整数向链表建立节点数据,输入负数结束建立\nlinklist = ");
while (1) {
scanf("%d", &data);
if (data < 0) {
break;
}
// 第一次输入
if (ll == NULL) {
ll = malloc(sizeof(Node));
// 增长程序健壮性
if (ll == NULL) {
exit(0);
}
// 赋值,改指针
ll->data = data;
ll->next = ll;
r = ll;
}
else {
Node *node = malloc(sizeof(Node));
if (node == NULL) {
break;
}
node->data = data; // 数据
node->next = r->next; // r是尾节点,r->next指向头节点,,,这一步是把头节点的地址给node->next,把node的next指向头节点
r->next = node; // 把node的地址给r->next,这一步是把node连到了链表的最后,此时node就是尾节点,r是倒数第二个节点
r = r->next; // 将r从新改到尾节点
}
}
return ll;
}
// 指定位置插入一个节点
hyStatus insertNode(LinkList *ll, int data, int index) {
if (index < 1) {
printf("插入位置非法\n");
return ERROR;
}
// 要插入的节点
Node *node = malloc(sizeof(Node));
if (node == NULL) {
exit(0);
}
node->data = data;
// 插入在第一个位置和插入在最后一个位置,链表的结构相同,但意义不一样
// 在最后一个位置插入时,*ll指针指向的还是头节点
// 在第一个位置插入时,*ll要修改指向为当前插入的节点,此时新插入的节点是头节点,本来的头节点如今已是第二个了,这也是为何参数要传*ll的缘由
if (index == 1) {
// 首先,要找到最后一个节点
Node *lastNode = *ll;
while (lastNode->next != *ll) {
lastNode = lastNode->next;
}
// 找到了最后一个节点,开始插入
node->next = *ll;
lastNode->next = node;
// 改变头节点的位置,为新插入的node
*ll = node;
}
else {
// 若是要插入第3个位置,则须要找到第2个位置
// 由于tNode默认就在第1个位置,因此只须要循环index-2次就可找到index前一个位置
Node *tNode = *ll;
for (int i = 1; i < index - 1; i++) {
if (tNode->next == *ll) {
printf("插入的位置比链表长度还大\n");
return ERROR;
}
tNode = tNode->next;
}
node->next = tNode->next;
tNode->next = node;
}
return SUCCESS;
}
// 删除指定位置的节点
hyStatus deleteNode(LinkList *ll, int index) {
if (index < 1) {
printf("删除位置非法\n");
return ERROR;
}
// 删除第一个节点
if (index == 1) {
// 首先,要找到最后一个节点
Node *lastNode = *ll;
while (lastNode->next != *ll) {
lastNode = lastNode->next;
}
// 找到了最后一个节点,开始删除第一个
lastNode->next = (*ll)->next;
(*ll)->data = 0;
(*ll)->next = NULL;
free(*ll);
// 改变头节点的位置,为本来的第二个节点
*ll = lastNode->next;
}
else {
// 若是要删除第3个位置,则须要找到第2个位置
// 由于tNode默认就在第1个位置,因此只须要循环index-2次就可找到index前一个位置
Node *tNode = *ll;
for (int i = 1; i < index - 1; i++) {
tNode = tNode->next;
if (tNode->next == *ll) {
printf("删除的位置比链表长度还大\n");
return ERROR;
}
}
// 开始删除
Node *delNode = tNode->next;
tNode->next = delNode->next;
delNode->data = 0;
delNode->next = NULL;
free(delNode);
}
return SUCCESS;
}
// 查询某值在链表中的位置
int findNode(LinkList ll, int data) {
Node *p = ll;
int index = 1;
do {
if (p->data == data) {
return index;
}
p = p->next;
index++;
} while (p != ll);
printf("没有找到此节点\n");
return ERROR;
}
// 遍历循环链表
void foreachLinkList(LinkList ll) {
Node *p = ll;
do {
printf("%-5d", p->data);
p = p->next;
} while (p != ll);
printf("\n");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 建立链表
LinkList ll = createLoopLinkList();
foreachLinkList(ll);
// 插入节点
printf("++++++++++++++++++++++++++\n");
insertNode(&ll, 0, 1);
foreachLinkList(ll);
// 删除节点
printf("--------------------------\n");
deleteNode(&ll, 1);
foreachLinkList(ll);
// 查询节点
printf("==========================\n");
printf("查找到的位置是:%d\n", findNode(ll, 10));
}
return 0;
}
复制代码
在上面的单向链表中能够发现一个问题,就是若是想要知道某个节点的地址,那么就必须先找到它的前一个节点,经过前一个节点的next才能获取这个节点的地址。同时咱们也不能经过当前节点得到前一个节点的地址,在操做过程当中就会感受很麻烦。
而接下来要说的双向链表就很好的解决了这种问题,单向链表的节点是只保存了数据和下一个节点的地址,那么能不能把前一个节点的地址也保存起来呢?答案是确定的。具体样式以下图:
在贴代码以前先说一下head节点,经过上面的单向链表代码能够知道,不管是在插入节点仍是删除节点,都不可避免的须要判断要插入的位置是否是第一个?要删除的节点是否是第一个节点?由于在进行插入和删除时,第一个节点和其余的节点须要进行不一样的操做。
那么如何让第一个节点和其余节点操做相同呢?这里就引入了头节点(head节点)的概念。其实头节点应该在链表建立的时候默认建立好,可是不让它存储数据。这时第一个节点实质上是第二个节点,由于真正的第一个节点是head节点,这样就保证了插入和删除节点的时候第一个节点和其余节点进行的操做相同。
光说不练假把式,看代码:
#import <Foundation/Foundation.h>
#define ERROR 0
#define SUCCESS 1
typedef int hyStatus;
typedef struct _Node {
int data;
struct _Node *frontNode; // 前驱
struct _Node *nextNode; // 后继
} Node;
typedef Node* LinkList; // 双向链表
// 单向链表中没有使用`head`节点,在这个双向链表中就使用一次`head`节点吧
// 建立一个链表
LinkList createLinkList() {
// 新建立的这个节点就是`head`节点
LinkList ll = malloc(sizeof(Node));
// 默认建立成功了,就不考虑开辟空间失败了😄
ll->data = 0; // 这里就用来保存节点的数量好了,反正闲着也是闲着
ll->frontNode = NULL;
ll->nextNode = NULL;
// 添加节点
Node *lastNode = ll; // 记录最后一个节点
int data = 0;
printf("请输入节点数据,负数表示输入结束:\n");
while (1) {
scanf("%d", &data);
if (data < 0) {
break;
}
// 建立节点
Node *node = malloc(sizeof(Node));
node->data = data;
node->nextNode = NULL;
// 将节点添加到链表后面
node->frontNode = lastNode;
lastNode->nextNode = node;
lastNode = node;
// 链表的节点数量++
ll->data++;
}
return ll;
}
// 插入一个节点
hyStatus insertNode(LinkList *ll, int data, int index) {
if (index < 1 || index > (*ll)->data + 1) {
printf("插入的位置不合法\n");
return ERROR;
}
// 建立一个节点
Node *node = malloc(sizeof(Node));
node->data = data;
node->frontNode = NULL;
node->nextNode = NULL;
// 开始插入,由于有head节点的存在,插入第1个位置和其余位置没太大区别
Node *p = *ll;
for (int i = 1; i < index; i++) {
// 为了保险起见,仍是加上这个判断吧
if (p->nextNode) {
p = p->nextNode;
}
}
// 开始插入节点
if (p->nextNode) {
node->nextNode = p->nextNode;
p->nextNode->frontNode = node;
}
p->nextNode = node;
node->frontNode = p;
// 链表长度++
(*ll)->data++;
return SUCCESS;
}
// 删除一个节点
hyStatus deleteNode(LinkList *ll, int index) {
if (index < 1 || index > (*ll)->data) {
printf("删除的位置不合法\n");
return ERROR;
}
Node *p = *ll;
for (int i = 1; i < index; i++) {
if (p->nextNode) {
p = p->nextNode;
}
}
// 开始删除
Node *delNode = p->nextNode; // 保存要删除的那个节点
p->nextNode = delNode->nextNode;
// 若删除的是最后一个节点,是没有nextNode的
if (delNode->nextNode) {
delNode->nextNode->frontNode = p;
}
// 开始释放要删除的节点
delNode->data = 0;
delNode->nextNode = NULL;
delNode->frontNode = NULL;
// 链表长度--
(*ll)->data--;
return SUCCESS;
}
// 查询节点
int findNode(LinkList ll, int data) {
Node *p = ll;
for (int i = 1; i <= ll->data; i++) {
// 由于p最开始是指向header节点的,先向后移一个节点
if((p = p->nextNode)) {
// 判断是否找到
if (p->data == data) {
return i;
}
}
else {
printf("没有找到\n");
return 0;
}
}
printf("没有找到\n");
return 0;
}
// 遍历链表
void foreachLinkList(LinkList ll) {
Node *p = ll->nextNode; // 第一个节点
while (p) {
printf("%-5d", p->data);
p = p->nextNode;
}
printf("\n");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 建立链表
LinkList ll = createLinkList();
foreachLinkList(ll); // 遍历链表
// 插入节点
printf("+++++++++++++++++++++++++++++++++++\n");
insertNode(&ll, 0, 4);
foreachLinkList(ll);
// 删除节点
printf("-----------------------------------\n");
deleteNode(&ll, 4);
foreachLinkList(ll);
// 查询节点
printf("===================================\n");
printf("查询的位置是:%d\n", findNode(ll, 7));
}
return 0;
}
复制代码
图有点丑,可是相信看完了上面那些,理解这个仍是很轻松的。
对于使用了head节点来讲,上图中的第一个节点就是head节点,第二个节点才是需求上须要使用的第一个节点。
双向循环链表代码实现:
#import <Foundation/Foundation.h>
#define ERROR 0
#define SUCCESS 1
typedef int hyStatus;
typedef struct _Node {
int data;
struct _Node *frontNode; // 前驱
struct _Node *nextNode; // 后继
} Node;
typedef Node* LinkList; // 双向链表
// MARK: - 单向链表和单向循环链表都没有使用head,那么在双向链表和双向循环链表中就都使用head好了,😂
// 建立链表
LinkList createLinkList() {
// MARK: - 所有复制过来,把首尾相连就行了
// 新建立的这个节点就是`head`节点
LinkList ll = malloc(sizeof(Node));
// 默认建立成功了,就不考虑开辟空间失败了😄
ll->data = 0; // 这里就用来保存节点的数量好了,反正闲着也是闲着
ll->frontNode = NULL;
ll->nextNode = NULL;
// 添加节点
Node *lastNode = ll; // 记录最后一个节点
int data = 0;
printf("请输入节点数据,负数表示输入结束:\n");
while (1) {
scanf("%d", &data);
if (data < 0) {
break;
}
// 建立节点
Node *node = malloc(sizeof(Node));
node->data = data;
node->nextNode = NULL;
// 将节点添加到链表后面
node->frontNode = lastNode;
lastNode->nextNode = node;
lastNode = node;
// 链表的节点数量++
ll->data++;
}
// 首尾相连
lastNode->nextNode = ll;
ll->frontNode = lastNode;
return ll;
}
// 插入一个节点
hyStatus insertNode(LinkList *ll) {
int data = 0, index = 0;
// 输入位置
printf("请输入插入节点的位置:");
scanf("%d", &index);
printf("\n");
// 判断输入的位置是否合法
if (index < 1 || index > (*ll)->data + 1) {
printf("插入的位置不合法\n");
return ERROR;
}
// 输入数据
printf("请输入插入节点的数据:");
scanf("%d", &data);
printf("\n");
// 建立节点
Node *node = malloc(sizeof(Node));
node->data = data;
node->nextNode = NULL;
node->frontNode = NULL;
// 找位置
Node *p = *ll;
for (int i = 1; i < index; i++) {
// 由于index通过判断,是合法的,因此不用考虑p->nextNode循环的问题
p = p->nextNode;
}
// 开始插入
node->nextNode = p->nextNode;
p->nextNode->frontNode = node;
p->nextNode = node;
node->frontNode = p;
// 长度++
(*ll)->data++;
return SUCCESS;
}
// 删除一个节点
hyStatus deleteNode(LinkList *ll) {
int index = 0;
printf("请输入要删除节点的位置:");
scanf("%d", &index);
printf("\n");
// 判断输入的位置是否合法
if (index < 1 || index > (*ll)->data) {
printf("删除的位置不合法\n");
return ERROR;
}
// 开始删除节点
Node *p = *ll;
for (int i = 1; i < index; i++) {
// 由于index通过判断,是合法的,因此不用考虑p->nextNode循环的问题
p = p->nextNode;
}
// 开始删除
Node *delNode = p->nextNode;
p->nextNode = delNode->nextNode;
delNode->nextNode->frontNode = p;
// 释放删除的节点
delNode->data = 0;
delNode->frontNode = NULL;
delNode->nextNode = NULL;
free(delNode);
// 链表长度--
(*ll)->data--;
return SUCCESS;
}
// 查询循环链表
int findNode(LinkList ll) {
int data = 0;
printf("请输入要查询的数据:");
scanf("%d", &data);
printf("\n");
// 开始查询
Node *p = ll->nextNode;
int index = 1;
while (p != ll) {
if (p->data == data) {
return index;
}
p = p->nextNode;
index++;
}
printf("没有找到这个节点\n");
return ERROR;
}
// 遍历循环链表
void foreachLinkList(LinkList ll) {
Node *p = ll->nextNode;
int index = 1;
while (p != ll) {
printf("%-5d", p->data);
p = p->nextNode;
index++;
}
printf("\n");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 建立链表
LinkList ll = createLinkList();
foreachLinkList(ll);
// 插入节点
printf("++++++++++++++++++++++++++++\n");
insertNode(&ll);
foreachLinkList(ll);
// 删除节点
printf("----------------------------\n");
deleteNode(&ll);
foreachLinkList(ll);
// 查找节点
printf("============================\n");
printf("查找数据的位置是:%d\n", findNode(ll));
}
return 0;
}
复制代码
好了双向循环链表到这也已经说完了,可能会感受这篇文章草草了事,可是我实在是以为这些没什么东西,细心思考应该都能实现这些功能。因此大部分文字都是用来解释一些名词,概念等。
至于逻辑思路,一切尽在代码中,一切尽在注释中。
这篇文章主要讲了线性表的链式存储结构------链表。 包括了单向链表、单向循环链表、双向链表、双向循环链表以及他们对应的建立链表、插入节点、删除节点、查询数据等功能。
之因此说这个,是由于我忽然想到了代码中用到了一些传地址的地方,可能对于一些C语言基础不是太好的读者来讲有点迷茫。
首先我相信你们应该都知道C语言函数的传参,实质是把变量的空间拷贝一份传进函数实现。如:
// 将传进来的参数+1
void sumOne(int a) {
a = a + 1;
}
int x = 10;
// 在调用这个方法的时候,实质是把x拷贝一份,而后把拷贝的变量传进函数实现
sumOne(x);
复制代码
由于是把变量拷贝了一份传到方法实现,而且这个拷贝出来的变量的做用域就只在sumOne
函数内。所以在sunOne
函数内的a = a + 1;
并不会改变函数外部的x
的值。
若是我想让函数改变外部变量的值怎么办?很简单,传地址。以下:
// 将传进来的参数+1
void sumOne(int *a) {
// a是地址,*a表明经过地址a找到对应的那块空间
*a = *a + 1;
}
int x = 10;
int *x1 = &x; // 将x的地址赋值给指针变量x1
// 这里调用方法的时候会将指针变量x1拷贝一份传进函数实现
// 即将x的地址拷贝一份传进函数实现
sumOne(x1);
复制代码
这样,就能够达到在函数内部修改外部变量值的效果了。
传地址的用法可不只仅是这样,咱们知道在OC中有block的存在来解决回调的需求,那么C语言的回调函数须要怎么搞?
没错,就是使用这个传地址,只不过传的地址是回调函数的函数地址,就这么简单。