考研数据结构——单链表
定义需要
#include <stdio.h>
#include <stdlib.h>
/**
定义单链表的结构体
*/
typedef struct LNode {
int data;
LNode* next;
} LNode, *LinkList;
1. 头插法初始化链表(带头结点)
- 首先L表示的就是头结点,而指向L的过程是头指针
- 为L即头结点开辟空间,初始L->next为 NULL:表示是空表。
- 创建一个临时结点node,用于插入
- 通过循环赋值的方式为链表初始化,我这采用的方式为先输入一个值,后开辟空间,这样判断不再增加结点的时候,就不用开辟空间了。
- ⭐ 头插法的思路:
- 创建一个新的临时结点(node), 先让这个临时结点指向第一个元素(L->next)。 即 node->next = L->next;
- 让头结点指向这个新创建的临时结点: L->next = node;
/**
1. 头插法初始化(带头结点单链表)
*/
void linkListHeadInsert(LinkList &L) {
// 先创建一个临时结点s
LNode* node;
int x;
// 为头结点开辟空间
L = (LinkList)malloc(sizeof(LNode));
// 头结点初始化指向为 NULL
L->next = NULL;
// 头结点初始值为 0
L->data = 0;
while(true) {
// 为新点击赋值
printf("请为新生成的结点赋值,如果不生成请输入-1\n");
scanf("%d", &x);
if(x==-1) {
// 如果x=-1,就不生成了,也就不用开辟新的空间了
break;
}
// 为新结点开辟空间
node = (LinkList)malloc(sizeof(LNode));
if(node == NULL) {
// 如果地址开辟失败
printf("地址开辟失败\n");
break;
}
// 为新结点赋值
node->data = x;
// 新的结点指向原来的第一个元素(即头结点指向的)
node->next = L->next;
// 头结点指向新结点
L->next = node;
}
}
2. 尾插法初始化链表
- 先为头结点开辟空间
- 创建一个临时的node,用于生成新的结点
- ⭐尾插法的关键:
- 要有一个始终指向尾部的一个结点end, 初始化的时候 end=L
- 具体的插入逻辑, 就是尾部结点end指向新插入的结点node即 end->next = node;
新插入的结点就是尾部了, 所以 end = node;
注意: 一定注意为node->next设置为NULL,或者在循环结束后给 end->next = NULL; 不然会死循环
/**
2. 尾插法初始化(带头结点单链表)
*/
void linkListEndInsert(LinkList &L) {
// 为头结点开辟一个空间
L = (LinkList)malloc(sizeof(LNode));
// 初始化头结点
L->data = 0;
L->next = NULL;
// 创建临时结点
LNode* node;
int x;
// 需要创建一个始终指向尾部的指针 , 初始就是L
LNode* end = L;
while(true) {
// 为新点击赋值
printf("请为新生成的结点赋值,如果不生成请输入-1\n");
scanf("%d", &x);
if(x==-1) {
// 如果x=-1,就不生成了,也就不用开辟新的空间了
break;
}
node = (LinkList)malloc(sizeof(LNode));
node->data = x;
// 因为新的结点要插入到尾部,所以每个都是最后一个,最后一个的next是null
// 这个最后一个指向为null也可以设置到循环外
node->next = NULL;
// 尾部插入的逻辑
// 因为end表示最后一个元素, 新的元素插入到它的next
end->next = node;
// 新插入的元素就是 最后一个了 即end
end = node;
}
}
3. 遍历链表
- 结点不为空,继续循环输出
- 每次输出后,指向后一个结点
/**
3. 循环遍历单链表
*/
void linkListPrint(LinkList L) {
// L指向过程是头指针, L是头结点
// 拿到第一个元素即,头结点指向的第一个元素
// 因为是带头结点的单链表, 所以先拿到头结点指向的第一个元素
LNode* node = L->next;
// 结点值不为空的时候,继续向下遍历(判断条件是NULL)
printf("链表的遍历:");
while(node != NULL) {
printf("->>>%d ", node->data);
// node指向下一个结点
node = node->next;
}
printf("\n");
}
4. 查找元素,返回结点地址,否则返回NULL
- 首先将头指针指向的,也就是第一个元素赋值给一个临时的结点变量node
- 通过while循环判断,该结点是否为NULL; 因为while是先判断后运行, 如果第一个元素就为空, 直接就执行到最后对的未找到语句。
- 每次判断当前结点值是否为要找到的值,如果是,就返回节点值。如果不是就继续往下循环。
/**
4. 查找元素, 将找到的节点返回。
*/
LNode* getElem(LinkList L, int el) {
// 第一个元素
LNode* node = L->next;
// 开始循环查找
while(node!=NULL) {
if(node->data == el) {
// 找到了该元素,返回该结点
return node;
}
// 如果本结点不是
node = node->next;
}
// 如果循环结束没找到
printf("\n\n没找到该节点\n\n");
return NULL;
}
5. 在链表第i个位置插入元素
- 首先将头结点赋值给一个循环的结点变量nodeNext
- 循环从1开始, 到i-1结束。其实就是循环 i-1次,⭐找到要插入位置的前一个结点的地址。⭐在循环的过程中要判断结点是否为NULL,如果产生为NULL,说明插入位置溢出了。
- 考虑一下边界条件 第一个位置与最后一个位置
- 1也就是插入到位置最开始, 此时循环并不会执行, 我们的nodeNext就是第一个元素前的元素(头结点)
- 考虑最后一个位置, 最后一个位置前的元素就是最后一个元素,也不会为NULL
- 所以循环的判断方案适合于 插入元素到第一个位置到最后一个位置的任何一个位置
- 插入的思想⭐
- 循环遍历结束后 nodeNext是要插入位置的前一个元素。
- 首先将插入位置的元素也就是 nodeNext->next赋值给node的next 即 node->next = nodeNext->netx , 防止找不到。
- 再将nodeNext->next = node; (新插入的元素放在插入位置的后面)
- 所以 结构为 nodeNext >> 新结点 >> 原来插入位置的结点
/**
5. 在链表的第i个位置插入元素
*/
void linkListInsertByIndex(LinkList &L, int i) {
// 用于往下循环的节点值
LNode* nodeNext = L;
// 创建一个插入的结点
LNode* node;
int e;
int j;
for(j=1 ; j<i; j++) {
// 我要遍历到插入位置的前一个
// 例如我要插入到第3个位置,我此时跑2次, 也就是我从头结点向后移动两次, 为第二个元素
if(nodeNext == NULL) {
// 如果在nodeT 在向后寻找的过程中就出现NULL,说明插入的位置溢出了
printf("\n\n 插入位置不正确 \n\n");
return;
}
nodeNext = nodeNext->next;
}
printf("请输入要插入的值:");
scanf("%d", &e);
// 为node开辟空间
node = (LinkList)malloc(sizeof(LNode));
if(node == NULL) {
printf("\n\n 开辟空间失败 \n\n");
return;
}
node->data = e;
// 首先新的结点指向我上面插入位置前一个元素所指向的元素
node->next = nodeNext->next;
// 前一个元素指向新的元素
nodeNext->next = node;
printf("插入成功!\n\n");
}
6. 删除指定位置的结点
- 删除的逻辑和插入一样,首先要先找到 要删除元素的前一个
- ⭐但是在边界控制条件上, 插入可以插入到最后一个元素后, 但是删除只能删除第一个元素到最后一个元素的位置, 所以判断的条件加一个 nodeNext->next == NULL(满足条件就是不合适删除的位置)
- 删除的逻辑
- 删除位置前的结点直接指向删除位置后的结点
- 释放掉要删除的结点
/**
6. 删除第i个位置的结点
*/
void linkListDelByIndex(LinkList &L, int i) {
// 创建一个循环往下的临时结点
LNode* nodeNext = L;
int j;
for(j=1 ; j<i; j++) {
// 我要遍历到删除位置的前一个
// 例如我要插入到第3个位置,我此时跑2次, 也就是我从头结点向后移动两次, 为第二个元素
// 删除逻辑, 元素在向后移动的时候,判断条件多了 nodeNext->next是否为空
// 这里的目的为了 限制最后一个元素的后一个位置(假如有七个元素, 插入位置为8可以,但是删除位置为8不是正确逻辑)
// (当有七个元素删除第8个的时候, 循环次数为 7, 所以要判断一下 下一个元素是否为NULL, 为NULL表示后面没有元素可以删除了)
if(nodeNext == NULL || nodeNext->next==NULL) {
// 如果在nodeT 在向后寻找的过程中就出现NULL,说明插入的位置溢出了
printf("\n\n 删除位置不正确 \n\n");
return;
}
nodeNext = nodeNext->next;
}
// 具体的删除逻辑
// 先将要删除的元素地址存下, 方便在删除后释放
LNode* nodeT = nodeNext->next;
// 将删除元素的前一个元素 直接指向 删除位置的下一个元素
// (这样即使删除的是最后一个元素, 删除元素位置前的元素最终指向的是 NULL, 她便成了最后一个元素)
nodeNext->next = nodeT->next;
// 释放删除了元素的空间
free(nodeT);
}
7. 求链表的长度
- 通过循环计数统计
- 初始赋值第一个元素为nodeNext
- 每次循环向后移动,直到nodeNext为NULL
/**
7. 求表长
*/
int getLinkListLen(LinkList L) {
// 设置一个用于循环的结点
LNode* nodeNext = L->next;
// 计数器
int count=0;
while(nodeNext!=NULL) {
// 第一次能进来说明, L(头结点有指向)
count++;
// 每次往后移动一次
nodeNext=nodeNext->next;
}
// 返回总长度
return count;
}
未完待续…