从链表开始
自链表开始,数据结构进入了一个新纪元,即大量使用结构体、结构体指针和内存空间申请malloc的时代。在开始链表的学习之前,我推荐所有朋友都认真地搞清楚结构体与结构体指针、malloc的具体用法和返回类型等,为从今往后的数据结构学习打好基础。
头插法与尾插法
顾名思义,头插法就是在链表的开头插入一个新节点,如果链表有头结点,那么就一直只在头结点后面插入新节点。而尾插法则是将新节点插入到链表的最后面,即链表的“小尾巴”。
详细代码实现
#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
typedef struct LNode {
ElemType data;
struct LNode* next;
}LNode, *LinkedList;
/**
* 头插法创建链表
* @param L 链表L的引用
* @return 链表L
*/
LinkedList HeadInsert(LinkedList& L){
LNode* s; //新建一个节点
ElemType x; //用于读入传入的数
//新建一个带头结点的链表
L = (LinkedList)malloc(sizeof(LNode));
L->next = NULL;
//用于读入输入的数据
printf("输入链表数据(以9999为结尾):\n");
scanf("%d", &x);
while (x != 9999){ //循环读入数据
s = (LinkedList)malloc(sizeof(LNode)); //为需要插入的s结点申请内存空间
s->data = x; //将值写入s结点
/*将s的下一结点指针指向头结点的下一结点指针
* 其中头结点第一次指向空,第二次指向上一个插入的结点
*/
s->next = L->next;
L->next = s; //将头结点的下一结点指针设置为要插入的结点s
//用于读入输入的数据
scanf("%d", &x);
}
return L;
}
/**
* 尾插法新建链表
* @param L 链表L的引用
* @return 链表L
*/
LinkedList TailInsert(LinkedList& L){
LNode* s; //创建两个新结点
LNode* r;
L = (LinkedList)malloc(sizeof(LNode));
L->next=NULL;
ElemType x;
r = L; //将L的地址赋给r
printf("输入链表数据(以9999为结尾):\n");
scanf("%d", &x);
while (x!=9999){
s = (LinkedList)malloc(sizeof(LNode)); //为s申请空间
s->data = x; //将值写入s结点
r->next = s; //上一个结点指向下一个结点
r = s; //将s作为最后一个尾结点
scanf("%d", &x);
}
r->next = NULL; //将最后一个结点的下一结点指针置为空,表示链表结束
return L;
}
/**
* 打印输出链表
* @param L 链表L
*/
void printList(LinkedList L){
L = L->next; //跳过头结点
while (L != NULL){
printf("%3d", L->data); //循环遍历打印
L = L->next;
}
printf("\n");
}
/**
* 查找第几个结点的值(默认头结点的下一个结点为第一个结点)
* @param L 链表L
* @param i 代表第几个节点
* @return 第i个位置的结点p
*/
LinkedList GetElem(LinkedList L, int i){
LinkedList p = L -> next; //跳过头结点
int j=1;
if (i == 0){ //如果只需要头结点
return L;
} else if (i <= 0){ //非法输入
return NULL;
}
while (p && (j < i)){ //在p不为空时向后遍历链表,直到到达相关数据
p = p->next;
j++;
}
return p;
}
/**
* 查找
* @param L 传入链表
* @param e 查找的数据
* @return 返回结点
*/
LinkedList LocateElem(LinkedList L, ElemType e){
LinkedList p = L->next; //跳过头结点
while (p!=NULL && p->data!=e){
p = p->next;
}
return p;
}
/**
* 往第i个位置插入元素
* @param L 传入链表
* @param i 插入元素的位置
* @param e 插入元素的内容
* @return 是否成功插入
*/
bool InsertList(LinkedList L, int i, ElemType e){
LinkedList p = GetElem(L, i-1); //获取想要插入的结点的前一个结点位置
if (p == NULL)
return false;
LinkedList s = (LinkedList)malloc(sizeof(LNode)); //为s申请空间
s->data = e; //将数据写入s
s->next = p ->next; //将s的下一结点指针指向p的下一结点指针
p ->next = s; //将p的下一结点指针指向s
return true;
}
/**
* 删除第i个位置的元素
* @param L 传入链表
* @param i 删除元素的位置
* @return 是否成功删除
*/
bool ListDelete(LinkedList L, int i){
LinkedList p = GetElem(L, i-1); //获取想要插入的结点的前一个结点位置
LinkedList s = p->next; //将要删除的结点写入s
if (p == NULL)
return false;
if (i <= 0)
return false;
p->next = s->next; //跳过s结点
free(s); //删除s结点
s = NULL; //避免野指针,将s置为null
return true;
}
int main(){
LinkedList L1;
LinkedList L2;
HeadInsert(L1); //头插法
TailInsert(L2); //尾插法
//打印输出
printList(L1);
printList(L2);
//获取某一位置的数据
LinkedList search1;
LinkedList search2;
search1 = GetElem(L1, 2);
search2 = GetElem(L2, 2);
if (search1 != NULL){
printf("按序号查找成功,值为:%d\n", search1->data);
}
if (search2 != NULL){
printf("按序号查找成功,值为:%d\n", search2->data);
}
//获取某一数据的位置的值
LinkedList search3;
LinkedList search4;
search3 = LocateElem(L1, 3);
search4 = LocateElem(L2, 3);
if (search1 != NULL){
printf("按内容查找成功,值为:%d\n", search3->data);
}
if (search2 != NULL){
printf("按内容查找成功,值为:%d\n", search4->data);
}
//插入链表结点,并打印输出
InsertList(L1, 2, 99);
InsertList(L2, 2, 99);
printList(L1);
printList(L2);
//删除链表结点,并打印输出
ListDelete(L1,4);
ListDelete(L2, 4);
printList(L1);
printList(L2);
return 0;
}
运行环境
macOS 11.6
CLion 2020.2
请注意不同环境的配置要求!!!!
运行结果
可见头插法的输出为倒序,尾插法输出为正序,其余功能均成功实现。
心得
数据结构最困难的一点就是要想办法去理解这些节点在内存内部是如何被操作的,而为了理解这一点,学习数据结构的朋友必须把握结构体和结构体指针的要义,明白结构体指针是如何操作内存中的数据的;再通过将自身逻辑带入内存中具体思考,数据结构的抽象逻辑便可迎刃而解。