2020-11-08 安装严蔚敏的教材全部重新提供接口
后山coder:C++中级程序员教程 全目录(必读)
本作业主要考察:C++复制控制/C++动态内存管理/基础线性容器实现/测试驱动开发/接口完备性
写在前面
第一章 背景介绍
一、单链表有什么用?
链表主要用来存放数据,比如一个班级的所有学生对象。
二、与原始的数组相比,我们为何需要链表?
因为,两个方面:
1 链表可以动态增加元素,而不像C语言的数组那样一开始固定长度。比如:
int a[10];//创建固定长度的数组
Student s[10];//创建固定长度的数组
链表就可以不断的往里面放数据,而不需要考虑能不能存放的下。比如:
List a;//创建一个链表对象
a.push_back(1);//将整数1放入链表
2 将数据及其操作看成一个整体(是一种数据结构)
C语言的数组也好,动态的内存也好,都是需要程序员自己去管理的。繁琐而且容易出错,但是链表就可以统一管理,代码的可读性也很好。
class List
{
public:
bool empty(void) const;//判断链表是不是空的
size_t size(void) const;//得到链表里有多少个元素
void push_back(int i);//往链表末尾追加一个新元素
void pop_back(void);//删除链表的最后一个元素
private:
//一些必要的成员变量
};
int main()
{
List l;
l.push_back(1);
if(l.size()>0)
{
//做一些事情
}
}
第二章 设计与分析
一、链表的样子
下面我们就一步一步设计链表这种结构
既然是链表,那就要前后链起来,变成一串。大概是这个样子
上面的链表只是示意图,链表总是要能够盛放数据的,不然也没有用处。
二、链表的元素
这里假设它盛放的一个一个的数字,干脆就是整数吧。像下面这样
让我们关注组成链表的一个一个的链表节点,不妨叫做Node。
Node里面放了一个整数。大致像下面这样:
//链表的节点
struct Node
{
int m_data;
};
这时候Node虽然没有链起来,但是却有存放整数的功能,我们不妨往里面存一个整数看看。例如,
#include <iostream>
//链表的节点
struct Node
{
int m_data;
};
int main(void)
{
Node node1;
Node node2;
node1.m_data = 1;
node2.m_data = 2;
std::cout << node1.m_data << ", " << node2.m_data << std::endl;
return 0;
}
如果我们愿意,我们可以创建很多node,然后逐个输出,这样好像看起来也是一个链表。
然而实际上它们并不是,因为它们散落在各个地方无法被统一管理(一盘散沙,这可不是我们想要的)。
三、将元素链起来
要想把各个节点Node链起来,还需要有一个链的功能,让一个Node具有可以找到下一个Node的能力。这时候我们可以把下一个Node的地址存在当前这个Node里。
这样以来,我们从第一个Node开始查看链表的时候,第一个Node除了能告诉我们它存的整数是多少,还能告诉我们下一个Node在哪里。
这个时候的链表应该更具体了,成了下面的样子。
注意观察,每一个节点被分成两部分:一部分用来存整数;另一部分用来存下一个节点在哪里。
这时候的Node结构也就变成了下面的样子,
//链表的节点
struct Node
{
int m_data;
Node* m_next;
};
现在,我们就让Node链起来看看,
#include <iostream>
//链表的节点
struct Node
{
int m_data;
Node* m_next;
};
int main(void)
{
std::cout << "Hello List!" << std::endl;
Node node1;
Node node2;
node1.m_data = 1;
node2.m_data = 2;
node1.m_next = &node2;//node1的下一个节点是node2,我们可以从node1找到node2
node2.m_next = nullptr;//下一个节点是空,表示这个节点是最后一个
//我们不再直接访问node2了,因为有node1这个领头的节点就可以了
std::cout << node1.m_data << ", " << node1.m_next->m_data << std::endl;
return 0;
}
我们做到了,
上面我们已经有了一个粗糙的链表。这个链表的头儿是node1,尾巴是node2。
我们也确实打印出了这个链表。但是这个链表的所有内容都暴露了出来。其实,我们只想拿到node1这个节点,也不想让链表的其他部分暴露出来。这样一个链表就像一个整体一样可以被传递,可以增加节点,可以删除节点。一个链表就是一个链表,而不是很多节点。
这样以来,我们就要有一个List结构来专门做这件事情,也就是上图中的那个L。
那么最简单的L就是一个存储着一个node节点地址的变量就可以了。比如,
//链表
struct List
{
Node* m_head;//存储链表的第一个节点来做到管理整个链表
};
有了List,我们就可以把上图中的那个L创造出来了。就像这样,
对应的代码,
#include <iostream>
//链表的节点
struct Node
{
int m_data;
Node* m_next;
};
//链表
struct List
{
Node* m_head;//存储链表的第一个节点来做到管理整个链表
};
int main(void)
{
Node node1;
Node node2;
node1.m_data = 1;
node2.m_data = 2;
node1.m_next = &node2;//node1的下一个节点是node2,我们可以从node1找到node2
node2.m_next = nullptr;//下一个节点是空,表示这个节点是最后一个
List L;
L.m_head = &node1;
//我们也不再直接访问node1了,因为有L了
std::cout << L.m_head->m_data << ", " << L.m_head->m_next->m_data << std::endl;
return 0;
}
输出当然还是一样,不会变化
四、将元素链起来封装成函数
上面有两个地方需要简化:
我们希望向链表里添加一个节点变的简单,不需要关注跟Node相关的细节。为此,我们封装一个函数,传进来一个整数,并将这个整数添加到链表里。这个函数不妨就叫PushBack吧。
五、输出一个链表
我们可以输出整个链表,我们希望有一个变量可以不断的往链表末尾走,从而每次都打印这个变量,来遍历整个链表。而不是像这样:L.mhead->mnext->m_next->......来得到后面的节点。所以我们有一个PrintList函数来做这件事情。
下面就是PushBack和PrintList加入进来之后的代码,
#include <iostream>
//链表的节点
struct Node
{
int m_data;
Node* m_next;
};
//链表
struct List
{
void PushBack(int value);
Node* m_head;//存储链表的第一个节点来做到管理整个链表
};
//打印整个链表
void PrintList(List l);//实现在后面
int main(void)
{
//纯粹的链表出现了,我们的思考方式再也不用关心Node是啥玩意儿了。
List list;
list.m_head = nullptr;//这是约定,要遵守,表示当前链表还没有一个数据
list.PushBack(1);
list.PushBack(2);
PrintList(list);
return 0;
}
void List::PushBack(int value)
{
//这时候我们并不知道链表目前有没有被插入过节点
auto fast = m_head;//fast沿着头节点往后跑,找到最后一个节点
auto slow = fast;//slow向跟屁虫一样,紧跟fast之后,用来对fast实施悬崖勒马,后面你会看到slow的妙处
while (fast != nullptr)
{
slow = fast;//跟屁虫跟上
fast = fast->m_next;//fast接着跑
}
//上面的循环不管如何,fast都会到达最后一个节点的下一个位置,也就是悬崖那里。而slow正好在前面一个节点
if (slow != nullptr)//链表至少有一个节点,slow指向链表的最后一个节点
{
//必须使用动态的堆内存上的对象,不然栈对象会在这个函数执行结束的时候释放掉
//那样我们的节点就被收回了
auto node = new Node;
slow->m_next = node;//由slow负责把新节点放到最后接上
node->m_data = value;//保存传进来的要被保存的整数数据
node->m_next = nullptr;//这是一种表示后面没有节点的约定
}
else//向空链表中增加第一个节点
{
m_head = new Node;
m_head->m_data = value;
m_head->m_next = nullptr;
}
}
void PrintList(List l)
{
auto p = l.m_head;
while (p != nullptr)
{
std::cout << p->m_data << ", ";
p = p->m_next;
}
}
当然输出就是我们期望的那样:
那么到目前为止,我们的单向链表就初步实现了。只不过这样的链表用处很受局限。
下面是作业,请你仿照上面的代码和思路实现下面的功能:
1 单链表的元素查找:
Node* FindInList(int value);//函数返回查找到值为value的第一个元素,没有就返回nullptr
2 实现单链表元素查找: (注意利用1)
bool ExistInList(int value);//函数返回value这个值是否在链表中,找到一个就算有。
3 实现单链表的节点删除:(注意利用1)
void DeleteValueInList(int value);//删除链表中的值为value的第一个元素,不存在什么也不做
4 实现链表当前元素数量:
size_t Size(void) const;//返回链表当前的容量
5 实现链表的默认构造函数
List();//实现链表成员变量的初始化
~List();//超出作用域的时候释放对象,同时负责释放自己开辟的动态内存
6 实现链表的复制构造函数
List(const List& listFrom);//拷贝一个新的链表,从listFrom拿数据
List& operator=(const List& listFrom);//将以对象赋值给另一个对象的时候
第三章 完整实现
一、带有头结点单链表
从第二章 四、PushBack函数的实现来看,我们的代码不够简洁,因为总是要在做一件事情之前先判断一下链表是否为空。这样的代码写起来容易出错,而且也不直观。
如果链表增加一个头结点,代码写起来就会舒服很多,虽然多了一个节点,但是好处远大于坏处(代码简洁)。
比如,插入元素就不用考虑链表是否为空:
void SLList::insert(int i, const ElementType & e)
{
if (i < 0 || i > m_size)
{
throw std::runtime_error("invalid index to insert.");
}
SLNode* p = &m_head;
int step = 0;
while (step < i && p != nullptr)
{
p = p->m_next;
++step;
}
SLNode* q = new SLNode(e);
q->m_next = p->m_next;
p->m_next = q;
++m_size;
}
二、带有头结点单链表的完整实现
代码如下(已包含大部分必要的注释):
#include <iostream>
#include <string>
typedef int ElementType;
class SLList
{
public:
SLList();//默认构造函数
SLList(const SLList& from);//复制构造函数
SLList& operator = (const SLList& from);//赋值操作符重载
~SLList();//析构函数
public:
class SLNode
{
friend class SLList;
private:
SLNode();
SLNode(const ElementType& t);
~SLNode();
public:
ElementType m_data;
SLNode* m_next;
};
public://成员函数
bool empty(void) const { return m_size == 0; }//判断链表是否为空
int size(void) const { return m_size; }//返回链表元素的数量
const ElementType& get_element(int i) const;//获取链表的第i个元素(可能会抛异常)
ElementType& get_element(int i);//获取链表的第i个元素(可能会抛异常)
/* 在第i个元素之前插入e(可能会抛异常)
拥有n个元素的链表可以插入的位置有n+1个:0, 1, ... , n
插入位置为n表示插入在最后一个元素之后
*/
void insert(int i, const ElementType& e);
/* 查找某个元素是否存在,当发现第一个时就返回,所以返回SLNode*;
这个函数有3个作用:
1 查找元素是否存在;
2 读写元素;
3 相当于返回的是迭代器
*/
SLNode* find(const ElementType& e);
void delete_element(int i);//删除第i个元素(可能会抛异常)
void clear(void);//删除所有元素
private:
void copy(const SLList & from);
private:
/* 指向头节点,不存储元素;
next为空指针表示没有最后一个元素,也就是空链表;
方便代码编写
*/
SLNode m_head;
int m_size;//链表当前有多少个元素
};
SLList::SLList()
: m_size(0)
{
std::cout << "SLList()n";
}
SLList::~SLList()
{
clear();
std::cout << "~SLList()n";
}
void SLList::copy(const SLList & from)
{
clear();
auto pThis = &m_head;
auto pFrom = &from.m_head;
for (int i = 0; i < from.m_size; i++)
{
auto q = new SLNode(pFrom->m_next->m_data);
pThis->m_next = q;
pThis = q;
pFrom = pFrom->m_next;
}
m_size = from.m_size;
}
SLList::SLList(const SLList & from)
{
if (!from.empty())
{
copy(from);
}
std::cout << "SLList(const SLList & from)n";
}
SLList & SLList::operator=(const SLList & from)
{
if (this == &from)
{
std::cout << "SLList & SLList::operator=(const SLList & from)n";
return *this;
}
else
{
copy(from);
std::cout << "SLList & SLList::operator=(const SLList & from)n";
return *this;
}
}
const ElementType& SLList::get_element(int i) const
{
if (i < 0 || i >= m_size)
{
throw std::runtime_error("invalid index of get_element.");
}
auto p = &m_head;
int j = 0;
while (p->m_next && j < i)
{
p = p->m_next;
++j;
}
return p->m_next->m_data;
}
ElementType & SLList::get_element(int i)
{
if (i < 0 || i >= m_size)
{
throw std::runtime_error("invalid index of get_element.");
}
auto p = &m_head;
int j = 0;
while (p->m_next && j < i)
{
p = p->m_next;
++j;
}
return p->m_next->m_data;
}
SLList::SLNode * SLList::find(const ElementType & e)
{
auto p = &m_head;
while (p->m_next)
{
if (p->m_next->m_data == e)
{
return p->m_next;
}
p = p->m_next;
}
return nullptr;
}
void SLList::insert(int i, const ElementType & e)
{
if (i < 0 || i > m_size)
{
throw std::runtime_error("invalid index to insert.");
}
SLNode* p = &m_head;
int step = 0;
while (step < i && p != nullptr)
{
p = p->m_next;
++step;
}
SLNode* q = new SLNode(e);
q->m_next = p->m_next;
p->m_next = q;
++m_size;
}
void SLList::delete_element(int i)
{
if (i < 0 || i >= m_size)
{
throw std::runtime_error("invalid index of delete_element.");
}
auto p = &m_head;
int j = 0;
while (p->m_next && j < i)
{
p = p->m_next;
++j;
}
auto q = p->m_next;
p->m_next = q->m_next;
delete q;
--m_size;
}
void SLList::clear(void)
{
for (auto p = &m_head; p->m_next != nullptr; )
{
auto q = p->m_next;
p->m_next = q->m_next;
delete q;
}
m_head.m_next = nullptr;
m_size = 0;
}
SLList::SLNode::SLNode()
:m_next(nullptr)
{
}
SLList::SLNode::SLNode(const int& t) : m_data(t), m_next(nullptr)
{
}
SLList::SLNode::~SLNode()
{
}
void print_list(const SLList& slist, const std::string& msg)
{
std::cout << "print " << msg << ":";
for (int i = 0; i < slist.size(); ++i)
{
std::cout << slist.get_element(i) << " ";
}
std::cout << "n";
}
void Check(bool b)
{
if (b)
{
std::cout << "Pass" << std::endl;
}
else
{
std::cout << "NOT Pass" << std::endl;
}
}
int main()
{
{
//test empty/size;
std::cout << "test empty/size" << std::endl;
SLList slist;
Check(slist.size() == 0);
Check(slist.empty());
slist.insert(0, 1);
Check(slist.size() == 1);
Check(slist.empty() == false);
}
{
//test insert/get_element/find;
std::cout << "test insert/get_element/find" << std::endl;
SLList slist;
Check(slist.size() == 0);
Check(slist.empty());
try
{
slist.get_element(-1);
}
catch (const std::exception& e)
{
std::cout<<"Pass " << e.what() << std::endl;
}
Check(slist.find(123) == nullptr);
slist.insert(0, 123);
Check(slist.find(123) && slist.find(123)->m_data == 123);
Check(slist.size() == 1);
Check(slist.empty() == false);
Check(slist.get_element(0) == 123);
slist.insert(1, 456);
Check(slist.find(456) && slist.find(456)->m_data == 456);
Check(slist.get_element(0) == 123);
Check(slist.get_element(1) == 456);
Check(slist.size() == 2);
try
{
slist.get_element(2);
}
catch (const std::exception& e)
{
std::cout << "Pass " << e.what() << std::endl;
}
slist.get_element(1) = 789;
Check(slist.get_element(1) == 789);
Check(slist.find(789) && slist.find(789)->m_data == 789);
}
{
//test delete_element;
std::cout << "test delete_element" << std::endl;
SLList slist;
try
{
slist.delete_element(0);
}
catch (const std::exception& e)
{
std::cout << "Pass " << e.what() << std::endl;
}
slist.insert(0, 123);
Check(slist.find(123) && slist.find(123)->m_data == 123);
slist.delete_element(0);
Check(slist.size() == 0);
Check(slist.empty());
slist.insert(0, 123);
slist.insert(1, 456);
slist.insert(2, 789);
slist.insert(3, 101112);
slist.insert(4, 131415);
print_list(slist, "5个整数");
slist.delete_element(slist.size() - 1);
print_list(slist, "删除最后一个,剩4个整数");
slist.delete_element(0);
print_list(slist, "删除第1个,剩3个整数");
slist.delete_element(1);
print_list(slist, "删除第2个,剩2个整数");
slist.delete_element(0);
slist.delete_element(0);
print_list(slist, "全部删除后");
}
{
//test clear;
std::cout << "test clear" << std::endl;
SLList slist;
slist.insert(0, 123);
slist.insert(1, 456);
slist.insert(2, 789);
slist.insert(3, 101112);
slist.insert(4, 131415);
print_list(slist, "5个整数");
slist.clear();
print_list(slist, "全部删除后");
}
{
//test copy;
std::cout << "test copy" << std::endl;
SLList slist;
slist.insert(0, 123);
slist.insert(1, 456);
slist.insert(2, 789);
slist.insert(3, 101112);
slist.insert(4, 131415);
print_list(slist, "slist 5个整数");
{
SLList slist2(slist);
print_list(slist2, "slist2 5个整数");
}
{
SLList slist3;
slist3 = slist;
print_list(slist3, "slist3 5个整数");
}
slist.clear();
print_list(slist, "全部删除后");
}
}
测试与输出:
test empty/size
SLList()
Pass
Pass
Pass
Pass
~SLList()
test insert/get_element/find
SLList()
Pass
Pass
Pass invalid index of get_element.
Pass
Pass
Pass
Pass
Pass
Pass
Pass
Pass
Pass
Pass invalid index of get_element.
Pass
Pass
~SLList()
test delete_element
SLList()
Pass invalid index of delete_element.
Pass
Pass
Pass
print 5个整数:123 456 789 101112 131415
print 删除最后一个,剩4个整数:123 456 789 101112
print 删除第1个,剩3个整数:456 789 101112
print 删除第2个,剩2个整数:456 101112
print 全部删除后:
~SLList()
test clear
SLList()
print 5个整数:123 456 789 101112 131415
print 全部删除后:
~SLList()
test copy
SLList()
print slist 5个整数:123 456 789 101112 131415
SLList(const SLList & from)
print slist2 5个整数:123 456 789 101112 131415
~SLList()
SLList()
SLList & SLList::operator=(const SLList & from)
print slist3 5个整数:123 456 789 101112 131415
~SLList()
print 全部删除后:
~SLList()
试试吧,祝你好运!