为了动态和灵活地增加、删除数据,可以通过指针动态地管理元素。链表的每个元素成为一个节点,每个节点的结构内都包含一个指向下一个节点的指针。通过这些节点的指针,最终可以将这些节点元素链接在一起,因此这种数据形式称为链表。
链表的一般定义形式:
struct film{char title[20];int rating;struct film * next;
};
next指针就是实现链接功能并且指向下一个节点的指针。在ADT的表述中,链表结构的功能函数包括创建链表、添加节点、删除节点、遍历节点等。
1.创建节点问题
template<class type>
struct Node{int data;Node<type>* next;//成员初始化列表的方式写构造函数Node() :next(nullptr){} Node(type data) :data(data), next(nullptr){}//定义一个带参数的构造函数
};
(1)成员变量声明
-
Node<type>
类型的指针:Node<type>
表示一个模板化的节点类型。其中,type
是模板参数,可以在实例化这个结构体时指定节点存储的数据类型。Node<type>*
就是一个指向这种类型节点的指针。
-
next
成员变量:
例如,假设有一个链表存储整数,代码可能如下:
Node<int> node1(5);
Node<int> node2(10);
node1.next = &node2;
//或是
node1->next = node2;
创建了两个节点node1
和node2
,并将node1
的next
指针指向node2
,表示它们在链表中是相邻的节点——或是,将node1
所指向的节点的next
成员变量设置为指向node2
所指向的节点,从而建立了链表中的连接关系。
(2)next
指针初始化为nullptr
一、安全性考虑
- 避免意外访问:
- 当创建一个新的节点时,如果不明确初始化
next
指针,它可能会指向一个随机的内存地址。在后续对链表进行操作时,如果不小心使用了未初始化的next
指针,可能会导致程序访问非法内存地址,从而引发严重的错误甚至崩溃。 - 通过初始化为
nullptr
,可以确保在使用next
指针之前,明确知道它没有指向任何有效的节点,从而避免意外访问无效内存。
- 当创建一个新的节点时,如果不明确初始化
二、明确链表结构
- 表示链表状态:
三、便于后续操作
- 简化插入和删除操作:
四、可读性和可维护性
- 提高代码的清晰度:
- 明确地将
next
指针初始化为nullptr
使得代码更加清晰易读。其他开发人员在阅读代码时,可以立即明白新创建的节点在初始状态下没有与其他节点建立连接,有助于更好地理解链表的操作逻辑。 - 这种明确的初始化方式也有助于提高代码的可维护性。在后续对代码进行修改和扩展时,不容易因为未初始化的指针而引入难以察觉的错误。
- 明确地将
(3)构造函数
- 无参构造函数的作用
- 初始化默认状态:
- 提供默认行为:
- 它为用户提供了一种创建节点的默认方式。如果用户只需要创建一个节点,而暂时不关心节点的数据部分(例如,数据部分可能会在后续步骤中填充),就可以使用无参构造函数。这增加了代码的灵活性,使得用户可以根据具体的需求选择合适的构造函数来创建节点。
- 有参构造函数的作用
- 方便数据初始化:
- 完整对象创建:
- 两个构造函数结合的优势
*注:
如果成员变量是常量或者引用类型,就必须使用成员初始化列表进行初始化。
2.链表模板类
template<class type>
class list{
public://无参构造list(){size = 0; //记录链表中节点的数量listEnd = nullptr;listHead = nullptr;}
一、模板化的优势
- 通用性:
二、无参构造函数的作用
- 初始化链表状态:
- 提供默认创建方式:
- 与其他操作的配合:
3. assign成员函数
void assign(int* begin, int* end){ //begin、end函数模板的参数while(begin != end){Node<type>* node = new Node<type>(*begin);if(size == 0){listHead = node;listEnd = node->next;}else{node->next = listHead;listHead = node; }size++;begin++;指向下一个数组元素的位置}
正序构建链表:
else{listEnd->next = node; // 将新节点加到链表的末尾//让当前链表的尾节点 listEnd 的 next 指针指向新的节点 node,将新节点连接到链表的末尾。listEnd = node; // 更新 listEnd 为新的末尾}
-
函数整体功能:
-
循环部分:
-
链表为空时的处理:
-
链表不为空时的处理:
else
:当链表不为空时,新创建的节点将被插入到链表的头部,形成一个逆序的链表。node->next = listHead
:将新创建的node节点的next
指针指向当前的头节点,即将新节点插入到链表的头部,实现链表的逆序插入操作。listEnd = node
:更新链表的尾指针为新创建的节点。这是因为在每次插入节点后,新节点成为了链表的最后一个节点。- 构建逆序链表的意图:在这段代码中,
assign
函数的实现方式是构建一个逆序的链表。当从数组中逐个取出元素创建节点并插入链表时,新节点会被插入到链表头部,所以新节点的next
指针需要指向当前的头节点。 - 例如,假设有数组元素
[1, 2, 3]
,第一次插入节点(值为 1)时,因为链表为空,该节点既是头节点也是尾节点。第二次插入节点(值为 2)时,新节点(值为 2)成为新的头节点,其next
指针指向之前的头节点(值为 1),这样就构建了一个逆序的链表结构,新插入的节点总是在头部,原来的头节点就变成了新头节点的下一个节点。 - 方便操作的考虑:这种方式在某些情况下是比较方便的。比如在频繁地从头部插入节点时,不需要遍历链表找到最后一个节点来插入新节点,只需要简单地更新头节点和新节点的
next
指针就可以完成插入操作。 - 另外,在实现一些特定的算法或者功能时,逆序链表可能会更符合逻辑。例如,如果要实现一个后进先出(LIFO)的数据结构(类似于栈),这种逆序链表的构建方式就很合适,新插入的元素(新节点)总是最先被访问(因为在头部)。
-
更新链表状态:
总结来说,这个assign
函数通过遍历整数数组,逐步构建链表,并且在链表为空和不为空的情况下分别进行了不同的处理,最终实现了从数组初始化链表的功能。同时,由于它是list
类的成员函数,所以可以直接访问和修改list
类的成员变量,如listHead
、listEnd
和size
。
关于Node<type>* node = new Node<type>(*begin);说明:
-
整体结构:
- 这是一个声明并初始化指针的语句。它创建了一个指向
Node<type>
类型对象的指针,并使用动态内存分配(new
关键字)为一个新的Node<type>
对象分配内存空间,然后将该指针指向这个新创建的对象。
- 这是一个声明并初始化指针的语句。它创建了一个指向
-
类型分析:
-
动态内存分配:
new Node<type>(*begin)
使用new
关键字进行动态内存分配,创建一个新的Node<type>
对象。括号中的部分(*begin)
是调用Node
模板结构体的有参构造函数,将指针begin
所指向的值作为参数传递给构造函数。- 这个构造函数会使用传入的 值 来初始化新创建的节点的数据成员,并将节点的
next
指针 初始化为nullptr
。
-
赋值给指针:
- 最后,将新创建的
Node<type>
对象的地址赋给指针node(
当使用new
关键字进行动态内存分配时,它会返回所分配内存的地址)
。这样,node
就可以用来访问和操作这个新创建的链表节点。
- 最后,将新创建的
例如,假设正在处理一个存储整数的链表,并且指针begin
指向一个整数数组的第一个元素。如果这个数组的第一个元素是 5,那么这行代码会创建一个新的链表节点,节点的数据成员被初始化为 5,并且next
指针初始化为nullptr
。然后,指针node
就指向这个新创建的节点。
总结来说,这行代码在链表的初始化过程中起着关键作用,它动态地创建新的链表节点,并使用数组中的值来初始化节点的数据成员,为构建链表提供了基础。
4.保护成员
protected:Node<type>* listHead;//指向头的指针Node<type>* listEnd;int size;
}
为什么写保护成员:
- 封装性和数据隐藏
- 信息隐藏原则:
- 提供统一接口:
- 可继承性和扩展性
- 便于派生类使用:
- 实现多态性和定制化:
-
Node<type>* listHead
:头节点 -
Node<type>* listEnd
:尾节点 -
int size
:节点数量