本博客来源于之前C++老师课堂小测和上课讲解的内容,小测的时候写代码很困难,课后又重新复习了一下。又结合了西北工业大学mooc上的内容,整理如下:
(初学者可能会有错误,欢迎大家批评指正,且仅用于个人学习使用,若有侵权必删除)
写在前面
- 主要参考:西北工业大学mooc;相当于对魏老师讲解的笔记。
- 使用:visual studio 2010
基本概念
链表的定义
下面是百度百科中对于链表的定义:
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
可以看到,“链表由一系列结点组成”,因此,首先要了解一下结点:
由定义可以看到,结点由两部分组成:
- 存储数据元素的数据域;
- 存储下一个结点地址的指针域;
写成C++代码为:
struct Node
{
ElemType data; //存储数据元素的数据域
Node* link; //存储下一个结点地址的指针域;
};
然后分别对数据域和指针域做进一步的解释:
- 数据域:
typedef int ElemType; // 建立了一个整型类型 ElemType
这里采用的是`typedef`进行定义的。然鹅我自己看的时候,根本不知道什么是`typedef`,查了很多博客才明白。所以在此做一个解释:
简单来说,
typedef
就是: 任何声明变量的语句前面加上typedef
后,原来是变量的都变成一种类型。
因此,可以这样使用:typedef int NUM; NUM a =10; // 或者NUM(a) = 10;
- 指针域:
由于结构体一经声明,就可以进行指针和引用的相关操作,所有是完全合理合法的。link成员表示指针域,存放此结点链接的下一个结点的地址。其示意图大概是这样的:
链表的分类
有了上面对于link指针域的了解,就可以对链表进行分类了。链表可以按照不同的分类方式,分成以下几种常见的类:
单链表
单链表某个结点的仅包含一个指向直接后继结点的指针域。
用代码对单链表结点做定义:
// 定义数据域
typedef int ElemType;
// 定义结点
struct LNode
{
ElemType data;
LNode* next;
};
// 定义单链表的指针类型LinkList
typedef LNode* LinkList;
双链表
双链表的每一个结点中都包含两个指针,一个指向它前面的结点,一个指向它后面的结点。其形式为:
其代码描述为:
typedef int ElemType;
struct DNode
{
ElemType data;
DNode* prev; * next;
};
typedef DNode* DLinkList;
循环链表
若单链表尾结点指向头节点而不是0,则该链表是循环单链表。同理,若双链表尾结点next指向头节点而不是0,头节点prev指向尾结点而不是0,则该链表是循环双链表。
单链表的创建
头插法(逆序)
头插法建立链表CreateLink(&L,n,input())
该方法先建立一个头节点*L,然后产生新结点,设置新结点的数据域;再将新结点插入到当前链表的表头,直至指定数目的元素都增加到链表中为止。其步骤为:
① 创建头节点*L
,设置*L
的next为NULL;
② 动态分配一个结点s,输入s的data;
③ 将s插入开始节点之前,头节点之后,即如下图所示:
④ 重复②~④步骤,加入更多的结点。
其代码为:
// 头插法创建单链表
# include <iostream>
using namespace std;
/* 定义数据域 */
typedef int ElemType;
/* 定义结点结构体 */
struct LNode
{
ElemType data;
LNode* next;
};
/* 定义单链表指针类型 */
typedef LNode* LinkList;
/* 从键盘获取数据 *ep,显然ep为指针 */
void input(ElemType *ep)
{
cout << "请您输入数据:";
cin >> *ep;
}
/* 创建单链表 */
void CreateLinkF(LinkList *L, int n, void(*input)(ElemType*))
{
LinkList s; // 创建一个结点地址s;
*L = new LNode; // 头结点,申请内存;
(*L) -> next = NULL; //一开始链表为空;
for(; n>0; n--)
{
s = new LNode; // 申请内存;
input( & s -> data); // 调用函数;
s -> next = (*L) -> next;
(*L) -> next = s; // 完成上面的头插法循环算法
}
}
/* 主函数 */
int main()
{
LinkList L;
int n;
cout << "请输入您要创建多大的链表:";
cin >> n;
CreateLinkF(&L,n,input);
cout << "创建完成!hahaha";
cout << endl;
system("pause");
return 0;
}
用visual studio执行如下:
结果为:
尾插法(顺序)
头插法建立的链表中结点的次序与元素输入的顺序相反,若希望两者次序一致,可采用尾插法建立链表。该方法是将新结点插到当前链表的末尾上,其步骤为:
① 创建头节点*L
,设置*L
的next为0,且令指针p指向*L
;
② 动态分配一个结点s,输入s的数据;
③ 将s插入到当前链表末尾;
④ 重复②~④步
其代码为:
// 尾插法创建单链表
# include <iostream>
using namespace std;
/* 定义数据域 */
typedef int ElemType;
/* 定义结点结构体 */
struct LNode
{
ElemType data;
LNode* next;
};
/* 定义单链表指针类型 */
typedef LNode* LinkList;
/* 从键盘获取数据,显然,ep为指针 */
void input(ElemType *ep)
{
cout << "请输入数据:";
cin >> *ep;
}
/* 尾插法创建单链表,这个注释就不写了,算法完全参照前面画的图*/
void CreateLinkR(LinkList *L, int n, void(*input)(ElemType*))
{
LinkList p,s;
p = *L = new LNode;
for(;n>0;n--)
{
s = new LNode;
input (& s->data);
p -> next = s;
p = s;
}
p -> next = NULL;
}
/* 主函数 */
int main()
{
LinkList L;
int n;
cout << "请输入链表的长度:";
cin >> n;
CreateLinkR(&L, n, input);
cout << "创建成功!hahaha" << endl;
system("pause");
return 0;
}
结果为:
销毁链表
销毁链表DestroyList(&L)
按照动态内存的使用要求,当不再使用链表时或程序结束前,需要将创建链表时分配的所有结点的内存释放掉,即销毁链表。
由于我们创建链表的时候,采用了动态内存分配的方法,因此,销毁链表是十分必要的。
销毁链表的步骤如下:
① 若*L
为0,表示已到链尾,销毁链表结束;
② 令指针p指向结点*L
的next,释放内存*L
;
③ *L
置换为p,即*L
指向直接后继节点,重复①~③步骤直至销毁链表结束。
其代码为:
// 销毁链表
void DestroyList(LinkList *L)
{
LinkList q,p = *L;
while (p != NULL)
{
q = p -> next;
delete p;
p = q;
}
*L = NULL;
}
链表的运算
链表遍历(打印 & 求长度)
链表遍历ListTraverse(L,visit())
与数组不同,链表不是用下标而是用指针运算查找数据元素的。通过链表的头指针L可以访问开始结点p=L->next
,令p=p->next
,即p指向直接后继节点,如此循环可以访问整个链表中的全部结点。
链表遍历算法的实现步骤为:
① 令指针p指向L的开始结点;
② 若p为0,表示已到链尾,遍历结束;
③ 令p指向直接后继节点,即p = p -> next
。重复②~③步骤直至遍历结束;
代码为:
// 遍历链表
/* 输出数据 */
void visit(ElemType *ep)
{
cout << *ep << " ";
}
/* 遍历函数 */
void ListTraverse(LinkList L, void(*visit)(ElemType*))
{
LinkList p = L -> next;
int len = 0;
while(p!=NULL)
{
visit(&(p->data));
len += 1;
p = p -> next;
}
cout << "您创建链表的长度为:" << len << endl;
}
结果为:
小结
今天先写到这里,明天写后半部分(插入、删除等和习题)。复习一下:
- 概念和分类;
- 创建(2种)
- 遍历