前言
"打牢基础,万事不愁" .C++的基础语法的学习
引入
链表是最基础的数据集合,对标数组.数组是固定长度,随机访问,链表是非固定长度,不能随机访问.数组查找快,插入慢;链表是插入快,查找慢.
前面推导过"数据结构+算法=数据集合".想建立一个数据集合,就要设计数据结构和他的算法.数据结构和算法是密不可分的.当然已经有优秀的前辈已经设计好了许多种数据结构供程序员选择使用.但学习最好是能知其所以然,
链表和数组是所有数据结构的底层.比如链栈,链队列,动态数组这些概念,表示其底层是链表或者数组.链表的内容如此少,以至于<C++ Prime Plus> 6th Edition(以下称"本书")都没写.我想学习的目的在于吸收设计者的思想,以及一些固定的解决问题模式.所以对链表内容做个理解.
链表的类表示
链表有数据域和指针域.数据域是数据集合的对象,指针域是链表本身的指针,他负责把数据链接在一起.以下为链表类示例,
typedef int Data;
class Node{
Data data; //数据域
Node* next; //指针域
};
Data是泛指类型,data表示泛型的单个对象,数据域不是固定的,他还可以是指向数组的指针Data*,表示数组作为单个元素组成的集合;或者指向其他结点(头结点)的指针,表示结点组成的集合.结点表示另一个链表.
指针域next也不是固定的,他还可以有其他指针,但类型必须是Node*,因为这样才可以把数据链接起来.例如加上一个Node* front,指向链表前一个元素,做成双向链表.next或者front只是符号,需要通过算法体现出来.
所以Node类是一个极简版本的链表.
链表的图示
结点采用了类包含数据的形式
前面的帖子C++基础语法:类包含-CSDN博客提到了类包含的用法,当时站在设计模式中的行为模式的角度去解释类包含,即类包含用于纳入已有行为类对象,形成一套解决方案.
结点是另一种类包含的用法,结点类包含的类对象,建立结点类,作为数据结构的基本元素
链表的算法
链表要做哪些事?他要建立一个集合,尾部指空;可以插入元素,删除元素,检索元素等,代码如下:
#include<iostream>
using namespace std;
struct Node {
//数据可见
int data;
Node* next;
};
class LinkList {
Node* firstNode; //头结点,表示整个链表
public:
/*初始化*/
LinkList() {
firstNode = new Node(); //建立头结点,数据空
}
/*插入int类型元素*/
void add(int data) {
Node* link = new Node(); //建立新结点
link->data = data; //传入数据
link->next = firstNode->next;
firstNode->next = link; //和上一行代码一起表示新结点插入头结点后面
}
/*查找数据,返回所在位置*/
int find(int data) {
Node* p = firstNode->next; //从第一个结点(非头结点)找起
int num = 0;
while (p) {
num++;
if (p->data == data)
return num;
p = p->next;
}
cout << "没找到数据" << endl;
return -1;
}
/*删除数据所在所有结点*/
bool erase(int data) {
Node* p = firstNode; //新建一个结点指向头结点
bool success = true; //声明标志位
/*下面如果用while(p), 将产生异常, 原因暂不知道*/
while (p->next) { //从第一个元素开始遍历链表
if (p->next->data == data) {//如果找到数据所在前一个结点
Node* tmp = p->next; //标识出要删除的结点
p->next = p->next->next;//前结点的next指向将被删除结点的后面
delete(tmp); //删除所在结点
success = false; //修改标志位
}
p = p->next; //指针指向下一个结点,把链表找遍,所有结点删除
}
if(success==false)
return true; //返回true
cout << "没找到数据:"<<data << endl;
return false; //遍历链表没找到数据,返回false
}
/*打印链表数据*/
int printLinkList() {
//下面这行如果用Node *p=firstNode;打印出来的是头结点值0
Node* p = firstNode->next; //从第一个结点(非头结点)开始打印
cout << "当前链表内数据如下:" << endl;
while (p) {
cout << p->data << endl;
p = p->next;
}
return 0;
}
};
测试代码:
//测试代码
int main(void) {
LinkList* p = new LinkList(); //建立链表,生成头结点
p->add(3);
p->add(4); //添加元素
p->add(5); //添加元素
p->add(6); //添加元素
p->add(4); //添加元素
p->printLinkList();
p->erase(4); //删除数据4所在所有结点
cout << "删除结点后链表内数据:" << endl;
p->printLinkList();
p->erase(7); //删除一个链表中没有的元素
return 0;
}
----说明: 用普通class类做的链表,没用template实现,结点固定了数据类型int
代码的几处说明:
1>为了方便书写,定义了struct结构的Node,相当于类数据不分私有private和公开public,全部公开,在LinkList类里可以用对象直接访问,省了一些步骤.如果用class定义Node,要在public里定义访问data的getData()方法,才能获得data.
2>头结点
在建立链表LiniList时建立了头结点firstNode,没有数据,指针指空.头结点是具体数据.通过头结点可以访问整个链表,所以头结点视为数据集合.在任何需要数据集合的地方,都需要使用头结点.如在erase和printLinkList中,遍历链表,都是声明一个指针,指向firstNode,凡是遍历链表的地方都是这个方法.初始化时就必须生成头结点
3> 插入和删除
在描述插入和删除这个地方要注意顺序:
插入时,先说明插入结点的后一个结点是什么,再说明他在哪一个结点后面
link->next = firstNode->next; //新结点插入头结点
firstNode->next = link;
删除时,先标记出结点,然后说明前后结点的关系,因为是单链表,所以只需要说明被删除结点的后一个结点,挂到他前一个结点的next即可(如果是双向链表,还要说明后一个结点的前面是被删除结点的前一个结点),然后用delete删除标识结点.
删除时有一点小问题,可看出双向链表的优点 ----了解
Node* tmp = p->next; //标识出要删除的结点
p->next = p->next->next;//前结点的next指向将被删除结点的后面
delete(tmp); //删除所在结点
插入和删除在初学时是一个难点,需要多想想.
4>头插法
每插入一个结点,都被放到头结点的后面,先前在头结点后面那个结点被放到新结点的后面.理解了插入,就明白了是怎么做的.
5>find函数
代码里定义了find函数,但没有使用他.
1.链表本身不支持随机访问,找到位置意义不大.----但想要也可以实现数组那样的"[]"下标功能
2.链表里可以多次出现同一个值,按照find函数定义只能找到最后插入的那个值的位置.所以严格来说这个函数存在bug.如果要找到最后一次出现的某个元素是第几次插入,倒是可以用find.操作步骤:先求出链表里所有元素的个数,再拿来减去find()函数的返回值.
3.起个抛砖引玉的作用.数据结构是一种逻辑结构,可以由需求定义各种各样的算法,并不局限于增删查等几种.比如还可以写出统计链表元素个数的算法,将链表元素逆序排列,像数组一样的随机访问[]等等(多种多样)
以链表为基础的数据结构
数据结构是有着某种逻辑结构的数据集合.数据集合的物理层是数组和链表.以链表为物理层的数据结构有链栈,链队列,二叉树等许多.他们有哪些共同特征呢?
一是有结点类,把将要加入数据结构的数据,放入结点
二是有头结点.在初始化容器时,把头结点建立起来.
三是有增删查算法(栈和队列不需要查,可以不定义).四根据需要开发其他算法.
小结
本贴没有用template泛型定义,适用面窄.用模板来做可以提高通用性,内部类可以方便实现更多功能,在后续数据结构中,用模板实现.
数据结构是编程比较难的部分,考验逻辑思维能力.编写的代码也容易出bug,要反复调试,考验耐心.但也比较有趣,可以自己提需求自己实现(连最基本的链表都有很多"玩法"),同时也是比较有价值的部分.虽然有很多成熟代码可以用,但是代码要多写手熟