数据结构实现(五):链表(C++版)
1. 概念及基本框架
链表 是一种 线性结构 ,而且存储上属于 链式存储(即内存的物理空间是不连续的),是线性表的一种。链表结构如下图所示:
下面以一个我实现的一个简单的链表类来进一步理解链表。
template <class T>
class Node{
public:
Node(T num = NULL, Node *next = NULL){
m_data = num;
this->next = next;
}
public:
T m_data;
Node *next;
};
首先设计一个 结点类 ,这个结点类包含 数据 和 指向下一个结点的指针 。结点类的构造函数可以直接对结点赋值,然后利用结点类对象来创建一个链表。链表类的设计如下:
template <class T>
class LinkedList{
public:
LinkedList(){
head.m_data = NULL;
head.next = NULL;
m_size = 0;
}
...
private:
Node<T> head;
int m_size;
};
这里为了避免重复设计就可以兼容更多数据类型,引入了 泛型 ,即 模板 的概念。(模板的关键字是 class 或 typename)
这里的 m_size 表示 链表长度 。为了保护数据,这个变量以及 结点数据 都设置为 private 。
与动态数组不同,动态数组的“动态”含义是可以自动扩容(缩容),但实质还是静态的,而链表则实现了真正意义上的动态。因为只有需要一个结点,才会添加一个结点,不需要就会删除。在这里,为了下面程序代码编写方便、统一,引入了 虚拟头结点(也称 哨兵结点 )的概念。这个结点本身不存放数据,用户也不知道它的存在。
实现了前面的程序之后,接下来就是一个链表的增、删、改、查以及一些其他基本操作,接下来利用代码去实现。
2. 基本操作程序实现
2.1 增加操作
template <class T>
class LinkedList{
public:
...
//增加操作
void add(int index, T num);
void addFirst(T num);
void addLast(T num);
...
};
首先,在类体内进行增加操作函数的原型说明。这里包括三个函数:
add(添加到任意位置)
addFirst(添加到头部)
addLast(添加到尾部)
然后分别去实现它们。
template <class T>
void LinkedList<T>::add(int index, T num){
if (index < 0 || index > m_size){
cout << "添加位置非法!" << endl;
return;
}
Node<T> *node = &head;
for (int i = 0; i < index; ++i, node = node->next);
node->next = new Node<T>(num, node->next);
m_size++;
}
template <class T>
void LinkedList<T>::addFirst(T num){
add(0, num);
}
template <class T>
void LinkedList<T>::addLast(T num){
add(m_size, num);
}
由于这些函数在类体外,所以每个函数头部必须添加一行代码:
template <class T>
表示该函数使用模板,下面同理。
如果不使用虚拟头结点,代码编写就要区分第一个结点和其他结点,从这里可以看出引入虚拟头结点的好处,统一了代码编写形式,下面的同理。
2.2 删除操作
template <class T>
class LinkedList{
public:
...
//删除操作
T remove(int index);
T removeFirst();
T removeLast();
void removeElement(T num);
...
};
同理,在类体内进行删除函数的原型说明。这里包括四个函数:
remove(删除任意位置元素):返回删除元素的值。
removeFirst(删除头部元素):返回删除元素的值。
removeLast(删除尾部元素):返回删除元素的值。
removeElement(删除特定元素):这里删除的是第一个这样的元素,如果想把这样的元素都删掉,可以写一个新的函数来实现。
然后分别去实现它们。
template <class T>
T LinkedList<T>::remove(int index){
if (index < 0 || index >= m_size){
cout << "删除位置非法!" << endl;
return NULL;
}
Node<T> *node = &head;
for (int i = 0; i < index; ++i, node = node->next);
Node<T> *p = node->next;
T res = p->m_data;
node->next = p->next;
delete p;
m_size--;
return res;
}
template <class T>
T LinkedList<T>::removeFirst(){
return remove(0);
}
template <class T>
T LinkedList<T>::removeLast(){
return remove(m_size - 1);
}
template <class T>
void LinkedList<T>::removeElement(T num){
Node<T> *node = &head;
Node<T> *p;
while(node){
p = node->next;
if (p->m_data == num){
node->next = p->next;
delete p;
m_size--;
return;
}
node = p;
}
}
这里删除操作的“删除位置非法”后面返回的 NULL 也可以用 throw 抛异常来实现,这里只是为了方便。
2.3 修改操作
template <class T>
class LinkedList{
public:
...
//修改操作
void set(int index, T num);
...
};
修改操作只有一个函数
set(修改指定位置的值)
同理,在类体内进行删除函数的原型说明,然后在类体外实现。
template <class T>
void LinkedList<T>::set(int index, T num){
Node<T> *node = head.next;
for (int i = 0; i < index; ++i, node = node->next);
node->m_data = num;
}
2.4 查找操作
template <class T>
class LinkedList{
public:
...
//查找操作
T get(int index);
T getFirst();
T getLast();
bool contains(T num);
...
};
查找函数有四个:
get(返回特定位置元素)
getFirst(返回第一个元素)
getLast(返回最后一个元素)
contains(返回是否包含特定元素)
这里并没有实现 find 函数,因为即使获得了元素位置索引,也不能像数组一样方便的再次通过位置索引获得数据,依然需要遍历链表来获得数据。
分别对它们进行实现。
template <class T>
T LinkedList<T>::get(int index){
if (index < 0 || index >= m_size){
cout << "访问位置非法!" << endl;
return NULL;
}
Node<T> *node = head.next;
for (int i = 0; i < index; ++i, node = node->next);
return node->m_data;
}
template <class T>
T LinkedList<T>::getFirst(){
return get(0);
}
template <class T>
T LinkedList<T>::getLast(){
return get(m_size - 1);
}
template <class T>
bool LinkedList<T>::contains(T num){
Node<T> *node = head.next;
while(node){
if (node->m_data == num){
return true;
}
node = node->next;
}
return false;
}
同理,这里 get 函数的“访问位置非法”后面返回的 NULL 也可以用 throw 抛异常来实现,这里只是为了方便。
2.5 其他操作
数组还有一些其他的操作,这些函数我在类体内进行了实现。
包括 链表长度 的查询,还有 链表的打印 等操作。
template <class T>
class LinkedList{
public:
...
int size(){
return m_size;
}
bool isEmpty(){
return m_size == 0;
}
void print(){
cout << "LinkedList: ";
cout << "Size = " << m_size << endl;
Node<T> *node = head.next;
while(node){
cout << node->m_data << "->";
node = node->next;
}
cout << "NULL" << endl;
}
...
};
3. 算法复杂度分析
3.1 增加操作
函数 | 最坏复杂度 | 平均复杂度 |
---|---|---|
add | O(n) | O(n/2) = O(n) |
addFirst | O(1) | O(1) |
addLast | O(n) | O(n) |
这里的时间复杂度与数组相反,原因在于,引起数组时间复杂度增加的是元素的移动,而引起链表时间复杂度增加的是元素的遍历。
3.2 删除操作
函数 | 最坏复杂度 | 平均复杂度 |
---|---|---|
remove | O(n) | O(n/2) = O(n) |
removeFirst | O(1) | O(1) |
removeLast | O(n) | O(n) |
同理,删除操作的时间复杂度与数组也相反。
3.3 修改操作
函数 | 最坏复杂度 | 平均复杂度 |
---|---|---|
set | O(n) | O(n/2) = O(n) |
3.4 查找操作
函数 | 最坏复杂度 | 平均复杂度 |
---|---|---|
get | O(n) | O(n/2) = O(n) |
getFirst | O(1) | O(1) |
getLast | O(n) | O(n) |
contains | O(n) | O(n/2) = O(n) |
总体情况:
操作 | 时间复杂度 |
---|---|
增 | O(n) |
删 | O(n) |
改 | O(n) |
查 | O(n) |
因为链表需要遍历,所以操作的复杂度都是 O(n) 级别的,但是,如果只针对头结点操作,那么操作时间复杂度就会变成 O(1) 级别。
4. 完整代码
程序完整代码(这里使用了头文件的形式来实现类)如下:
#ifndef __LINKEDLIST_H__
#define __LINKEDLIST_H__
using namespace std;
template <class T>
class Node{
public:
Node(T num = NULL, Node *next = NULL){
m_data = num;
this->next = next;
}
public:
T m_data;
Node *next;
};
template <class T>
class LinkedList{
public:
LinkedList(){
head.m_data = NULL;
head.next = NULL;
m_size = 0;
}
int size(){
return m_size;
}
bool isEmpty(){
return m_size == 0;
}
void print(){
cout << "LinkedList: ";
cout << "Size = " << m_size << endl;
Node<T> *node = head.next;
while(node){
cout << node->m_data << "->";
node = node->next;
}
cout << "NULL" << endl;
}
//增加操作
void add(int index, T num);
void addFirst(T num);
void addLast(T num);
//删除操作
T remove(int index);
T removeFirst();
T removeLast();
void removeElement(T num);
//修改操作
void set(int index, T num);
//查找操作
T get(int index);
T getFirst();
T getLast();
bool contains(T num);
private:
Node<T> head;
int m_size;
};
template <class T>
void LinkedList<T>::add(int index, T num){
if (index < 0 || index > m_size){
cout << "添加位置非法!" << endl;
return;
}
Node<T> *node = &head;
for (int i = 0; i < index; ++i, node = node->next);
node->next = new Node<T>(num, node->next);
m_size++;
}
template <class T>
void LinkedList<T>::addFirst(T num){
add(0, num);
}
template <class T>
void LinkedList<T>::addLast(T num){
add(m_size, num);
}
template <class T>
T LinkedList<T>::remove(int index){
if (index < 0 || index >= m_size){
cout << "删除位置非法!" << endl;
return NULL;
}
Node<T> *node = &head;
for (int i = 0; i < index; ++i, node = node->next);
Node<T> *p = node->next;
T res = p->m_data;
node->next = p->next;
delete p;
m_size--;
return res;
}
template <class T>
T LinkedList<T>::removeFirst(){
return remove(0);
}
template <class T>
T LinkedList<T>::removeLast(){
return remove(m_size - 1);
}
template <class T>
void LinkedList<T>::removeElement(T num){
Node<T> *node = &head;
Node<T> *p;
while(node){
p = node->next;
if (p->m_data == num){
node->next = p->next;
delete p;
m_size--;
return;
}
node = p;
}
}
template <class T>
void LinkedList<T>::set(int index, T num){
Node<T> *node = head.next;
for (int i = 0; i < index; ++i, node = node->next);
node->m_data = num;
}
template <class T>
T LinkedList<T>::get(int index){
if (index < 0 || index >= m_size){
cout << "访问位置非法!" << endl;
return NULL;
}
Node<T> *node = head.next;
for (int i = 0; i < index; ++i, node = node->next);
return node->m_data;
}
template <class T>
T LinkedList<T>::getFirst(){
return get(0);
}
template <class T>
T LinkedList<T>::getLast(){
return get(m_size - 1);
}
template <class T>
bool LinkedList<T>::contains(T num){
Node<T> *node = head.next;
while(node){
if (node->m_data == num){
return true;
}
node = node->next;
}
return false;
}
#endif