文章目录
- 链式存储
- 单链表
- 函数声明
- 双链表
- 函数的声明
链式存储
上一节课学习了顺序表存储,知道了其工作原理和优缺点,下面将开始学习另一种存储方式:链式存储,俗称链表。
顾名思义,链表就是用一个类似于"链条"的东西将数据一个一个连接起来。所以可以得知,它是由两部分组成,一部分为数据域(存放数据的地方),另一部分为指针域(“链条”)。
简单的介绍了一下链表的含义及组成,可以发现,它跟顺序表有很多区别:
- 顺序表存储数据是开辟一整块大的空间,按(下标)顺序进行分配空间,也就是每个数据之间的地址是连续的。而链表却恰好相反,它是每次使用时随机开辟一小块空间(一个数据域和指针域的空间),所以其相邻数据之间的地址不是连续的,而是随机的。
- 顺序表查询速度快、操作表(插入、删除)速度慢;而链表因为数据之间地址随机,只能根据一个节点中的指针域去查询到下一个数据,所以查询速度慢(时间复杂度为O(N)),而它在插入和删除的速度很快(忽略查询到目标位置的时间),其时间复杂度为O(1),所以对于一些需要频繁操作表数据的存储情况,使用链表存储的效率要高于顺序表。
由此可以看出,链表的优缺点也一目了然,查询速度慢,但操作速度(插入、删除)快。
下面,将用代码实现链表:
单链表
单链表也是最简单且最经典的链式结构,其组成结构也是最简单的,只有一个数据域和一个指向下一个数据域的指针域。
代码实现
函数声明
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct Node{
int val;//数据域
struct Node *pNext;//指向下一个数据的指针
}node,*pNode;//node是该结构体的名称,而pNode是该结构体的指针
pNode init();//初始化链表
bool add(pNode head,int len);//添加数据,len是添加长度
bool isEmpty(pNode head);//判断链表是否为空
bool insert(pNode head,int position,int val);//在position插入数据,val是插入的值
int length(pNode head);//求出链表的长度
int find_for_position(pNode head,int position);//查询position位置上的值,返回该节点的值
pNode find_for_val(pNode head,int val);//查询并返回值为val的节点
void traverse(pNode head);//遍历链表
bool delete_for_position(pNode head,int position);//删除指定位置的节点
bool delete_for_val(pNode head,int val);//删除值为val的节点
void close(pNode head);//关闭链表,释放空间
int main(){
return 0;
}
下面将对上述函数进行实现并测试
初始化链表
pNode init(){
pNode head = (pNode) malloc(sizeof (node));//动态创建一个头结点
if(head==NULL){//如果空间分配失败,则强制退出程序
printf("分配失败,程序终止!\n");
exit(-1);
}
return head;
}
添加数据
单链表的添加操作一般使用的是尾插法。
bool add(pNode head,int len){
pNode temp = head;//用一个指针指向头结点,方便进行下面的操作
for(int i = 0;i<len;i++){
int val = 0;//需要添加的数据
printf("请输入需要添加的值:\n");
scanf("%d",&val);
pNode pnew = (pNode) malloc(sizeof (node));
if(pnew==NULL){//如果空间分配失败,则强制退出程序
printf("空间分配失败!\n");
return false;
}
pnew->val = val;
temp->pNext = pnew;
pnew->pNext = NULL;//将新添节点进行悬挂
temp = pnew;//temp指向新生节点,方便后续继续添加
}
return true;
}
遍历链表(先实现该函数,方便后续DeBug)
void traverse(pNode head){
pNode temp = head->pNext;//因为头结点并不参与存储数据,所以要从头结点的下一个节点开始遍历
while(temp!=NULL){
printf("%d ",temp->val);
temp = temp->pNext;
}
printf("\n");
}
到此已经实现了三个函数,先编写main函数进行测试一下
int main(){
pNode head = init();
int n;
printf("请输入需要添加节点个数:\n");
scanf("%d",&n);
if(!add(head,n)){
printf("添加失败!\n");
}else{
traverse(head);
}
return 0;
}
测试结果为:
说明上面的几个函数代码实现没有问题,接下来,实现其他函数。
链表长度
int length(pNode head){
int num = 0;
pNode temp = head -> pNext;
while(temp != NULL){
temp = temp -> pNext;
num++;
}
return num;
}
判断链表是否为空
bool isEmpty(pNode head){
if(head->pNext!=NULL||head!=NULL){
return false;
}
return true;
}
插入数据
bool insert(pNode head,int position,int val){
pNode temp = head;
int len = length(head);//求出链表长度
if(position<0||temp==NULL) {
return false;
}
pNode pnew = (pNode)malloc(sizeof(node));
pnew->val = val;
if(pnew==NULL){
printf("动态分配内存失败!\n");
return false;
}
if(position>=len){//如果插入位置超出链表长度,则直接添加在现链表尾部
while(temp -> pNext != NULL){
temp = temp -> pNext;
}
}else{
for(int i = 0;i<position-1;i++) {
temp = temp->pNext;
}
}
pnew->pNext = temp->pNext;
temp->pNext = pnew;
return true;
}
到此,又实现了几个函数,接下来再进行测试
int main(){
pNode head = init();
int n;
printf("请输入需要添加节点个数:\n");
scanf("%d",&n);
if(!add(head,n)){
printf("添加失败!\n");
}else{
printf("插入数据前:");
traverse(head);
}
printf("该链表长度为:%d\n", length(head));
if(isEmpty(head)){
printf("链表为空\n");
}else{
printf("链表不为空\n");
}
insert(head,0,4);//在位置0(头节点)处插入4
printf("插入数据后:");
traverse(head);
return 0;
}
运行结果为:
接下来是实现查询函数,其中包含了两个查询,一个是按位置查询,一个是按节点中数值的值查询并返回该节点
按指定位置查询
int find_for_position(pNode head,int position){
int len = length(head);
if(position<0||position>len){
//如果位置非法,则返回-1
return -1;
}
//循环到position节点
for(int i = 0;i<position;i++){
head = head->pNext;
}
//找到则返回该节点的值
return head->val;
}
按值查询
pNode find_for_val(pNode head,int val){
while(head!=NULL){
if(head->val==val){
return head;
}
head = head -> pNext;
}
return NULL;
}
进行函数功能的测试
int main(){
pNode head = init();
int n;
printf("请输入需要添加节点个数:\n");
scanf("%d",&n);
if(!add(head,n)){
printf("添加失败!\n");
}
printf("该节点的值为:%d\n",find_for_position(head,4));
printf("该节点的值为:%d\n",find_for_position(head,3));
int num;
printf("请输入要查询的值:\n");
scanf("%d",&num);
pNode temp = find_for_val(head, num);
if(temp!=NULL){
printf("该节点的值为:%d\n",temp->val);
}else{
printf("NULL\n");
}
return 0;
}
测试结果为:
最后是删除结点的功能
删除目标值的结点
bool delete_for_val(pNode head,int val){
int i=0;
pNode ptemp = head;
while(ptemp->pNext!=NULL&&ptemp->pNext->val!=val){
ptemp = ptemp->pNext;
i++;
}
if(ptemp->pNext==NULL) {
printf("抱歉,删除失败!\n");
return false;
}
pNode p = ptemp->pNext;
ptemp->pNext = ptemp->pNext->pNext;
free(p);
p = NULL;//释放删除结点的内存
printf("成功删除位置%d的数据%d\n",i,val);
return true;
}
删除指定位置的结点
bool delete_for_position(pNode head,int position){
int len = length(head);//求出链表的长度
pNode temp = head;
if(position>len||position<0||len==0){
printf("删除失败!");
return false;
}
for(int i=0;i<position-1;i++){
temp = temp->pNext;
}
pNode p = temp->pNext;
temp->pNext = temp->pNext->pNext;
free(p);//释放删除结点的内存
p = NULL;
printf("删除成功!\n");
return true;
}
删除功能也实现完毕,最后,实现关闭链表的函数
void close(pNode head){
head = NULL;
free(head);
}
所有函数全部实现,接下来进行测试删除结点函数
int main(){
pNode head = init();
int n;
printf("请输入需要添加节点个数:\n");
scanf("%d",&n);
if(!add(head,n)){
printf("添加失败!\n");
}else{
printf("插入数据前:");
traverse(head);
}
printf("该链表长度为:%d\n", length(head));
if(isEmpty(head)){
printf("链表为空\n");
}else{
printf("链表不为空\n");
}
insert(head,0,4);
printf("插入数据后:");
traverse(head);
delete_for_val(head,4);//删除值为4的结点
delete_for_position(head,1);//删除位置1的结点
printf("删除数据后:");
traverse(head);
close(head);//最后要关闭链表
return 0;
}
运行结果:
到此,一个简单的单链表的全部功能已经实现。
双链表
双链表是指拥有一个数据域和两个指针域的链式存储结构,其中的两个指针域,一个指向前节点,另一个指向下一个节点。
代码实现
函数的声明
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct Node{
int val;//数据域
struct Node *pBefore;//指向前一个数据的指针
struct Node *pNext;//指向下一个数据的指针
}node,*pNode;//node是该结构体的名称,而pNode是该结构体的指针
pNode init();//初始化链表
bool order_insert(pNode head,pNode tail);//头插法,从头节点插入节点
bool disorder_insert(pNode head,pNode tail);//尾插法,从链表的末尾插入节点
bool isEmpty(pNode head,pNode tail);//判断链表是否为空
int length(pNode head);//求出链表的长度
void traverse(pNode head,pNode tail);//顺序遍历链表
void reverse(pNode head,pNode tail);//逆序遍历链表
int find_for_position(pNode head,pNode tail,int position);//查询position位置上的值,返回该节点的值
pNode find_for_val(pNode head,pNode tail,int val);//查询并返回值为val的节点
bool delete_for_position(pNode head,pNode tail,int position);//删除指定位置的节点
bool delete_for_val(pNode head,int val);//删除值为val的节点
void close(pNode head,pNode tail);//关闭链表,释放空间
从函数的声明可以看出,双链表比单链表多了一种插入方式(头插法),逆序遍历,其实这些东西单链表也可以实现,但相比之下双链表实现这些功能更加方便,也更加实用。
下面将从链表的初始化、插入节点、查询、删除等方式分类实现这些函数
初始化链表
双链表的初始化和单链表的初始化一样
pNode init(){
pNode Node = (pNode)malloc(sizeof(node));
return Node;
}
判断链表是否为空
bool isEmpty(pNode head,pNode tail){
if(head==NULL||tail==NULL||head==tail){
return true;
}
return false;
}
求出链表的长度
int length(pNode head,pNode tail){
int num = 0;
pNode temp = head -> pNext;
while(temp != NULL&& temp != tail){
temp = temp -> pNext;
num++;
}
return num;
}
插入节点
头插法
bool order_insert(pNode head,pNode tail){
//插入节点的个数
int len;
//创建一个临时节点替代tail
pNode temp = tail;
printf("请输入插入节点个数:\n");
scanf("%d",&len);
for(int i=0;i<len;i++){
pNode pnew = (pNode)malloc(sizeof(node));
if(pnew == NULL){
printf("内存分配失败!\n");
return false;
}
printf("请输入插入的值:\n");
scanf("%d",&pnew->val);
pnew->pNext = temp;
temp->pBefore = pnew;
pnew->pBefore = NULL;
temp = pnew;
}
head->pNext = temp;
temp->pBefore = head;
return true;
}
尾插法
bool disorder_insert(pNode head,pNode tail){
//插入节点的个数
int len;
//创建一个临时节点替代head
pNode temp = head;
printf("请输入插入节点个数:\n");
scanf("%d",&len);
for(int i = 0;i<len;i++){
pNode pnew = (pNode)malloc(sizeof(node));
if(pnew == NULL){
printf("内存分配失败!\n");
return false;
}
printf("请输入插入的值:\n");
scanf("%d",&pnew->val);
temp->pNext = pnew;
pnew->pBefore = temp;
pnew->pNext = NULL;
temp = pnew;
}
temp->pNext = tail;
tail->pBefore = temp;
return true;
}
这两种插入方式,原理如下图所示
查询
根据节点位置查询
int find_for_position(pNode head,pNode tail,int position){
int len = length(head,tail);
if(position<0||position>len){
//如果位置非法,则返回-1
return -1;
}
//创建一个临时节点
pNode temp;
if(position>=len/2){//如果查询的位置大于链表一半的长度,则从尾开始查询效率更高
temp = tail->pBefore;
for(int i = len;i>position;i--){
temp = temp->pBefore;
}
}else{//否则从头开始找
temp = head->pNext;
for(int i = 0;i<position;i++){
temp = temp->pNext;
}
}
//找到则返回该节点的值
return temp->val;
}
根据值查询,因为无法知道值的大致位置,所以这个函数实现方式和单链表一致,都是从头开始查询。
pNode find_for_val(pNode head,int val){
while(head!=NULL){
if(head->val==val){
return head;
}
head = head -> pNext;
}
return NULL;
}
删除
根据位置删除,与根据位置查询有异曲同工之处
bool delete_for_position(pNode head,pNode tail,int position){
int len = length(head,tail);//求出链表的长度
if(position>len||position<0||len==0){
printf("删除失败!");
return false;
}
pNode temp;//创建临时节点
if(position>=len/2){//如果查询的位置大于链表一半的长度,则从尾开始查询效率更高
temp = tail->pBefore;
for(int i = len;i>position;i--){
temp = temp->pBefore;
}
temp = temp -> pBefore;
}else{//否则从头开始找
temp = head->pNext;
for(int i = 0;i<position-1;i++){
temp = temp->pNext;
}
}
pNode p = temp->pNext;
temp->pNext = temp->pNext->pNext;
temp->pNext->pBefore = temp;
free(p);//释放删除结点的内存
p = NULL;
printf("删除成功!\n");
return true;
}
根据值删除
bool delete_for_val(pNode head,int val){
int i=0;
pNode ptemp = head;
while(ptemp->pNext!=NULL&&ptemp->pNext->val!=val){
ptemp = ptemp->pNext;
i++;
}
if(ptemp->pNext==NULL) {
printf("抱歉,删除失败!\n");
return false;
}
pNode p = ptemp->pNext;
ptemp->pNext = ptemp->pNext->pNext;
free(p);
p = NULL;//释放删除结点的内存
printf("成功删除位置%d的数据%d\n",i,val);
return true;
}
关闭链表
void close(pNode head,pNode tail){
head = NULL;
free(head);
tail = NULL;
free(tail);
}
此时,所有函数全部已经实现了,开始测试
int main(){
//初始化头指针和尾指针
pNode head = init();
pNode tail = init();
//头尾指针互相链接
head->pNext = tail;
head->pBefore = NULL;
tail->pNext = NULL;
tail->pBefore = head;
disorder_insert(head,tail);//尾插法
traverse(head,tail);//顺序遍历
reverse(head,tail);//逆序遍历
delete_for_position(head,tail,2);
delete_for_val(head, 4);
traverse(head,tail);
close(head,tail);
return 0;
}
运行结果如下:
这便是双链表的基本功能,对比单链表,它在查询数据时多了一种从尾开始查询,这样提高了查询的效率。