数据结构之链表
说明
链表是数据结构中的线性结构,用于存储一系列元素(节点),其中每个元素都包含一个指向下一个元素的引用。
链表由一组节点组成,每个节点包含两个部分:数据和指向下一个节点的指针(或引用)。
线性结构中对比数组/列表的优势:插入和删除性能较好
涉及的概念:
1. 节点:节点包括2个域,元素域、链接域
2. 元素域也叫数据域:就是链表在该节点上存储的数据(元素)
3. 链接域也叫指针域:建立当前节点与前后节点的关系。比如:单链表就是当前节点存储其下个节点地址的指针。
分类
链表有多种类型,包括单链表、双链表和循环链表等。
单链表(Singly Linked List)
单链表(Singly Linked List)是一种常见的链表数据结构,它由一组节点组成,每个节点包含两部分信息:
- 数据域:存储实际的元素值。
- 指针域(或引用域):指向下一个节点的引用或指针。
单链表的特点是每个节点只有一个指针,它指向链表中的下一个节点,而最后一个节点的指针通常为空,表示链表的结束。
单链表的第一个节点被称为头节点(Head),而最后一个节点的指针为空。链表的头节点用于表示链表的起始位置,通过头节点可以访问整个链表。
下面是一个简单的单链表示例:
[Head] -> [Node2] -> [Node3] -> [Node4] -> null
在上面的示例中,链表的头节点指向第2个节点Node2,Node2的指针指向Node3,依此类推,最后一个节点Node4的指针为空,表示链表结束。
单链表常用于需要频繁插入和删除元素的场景,因为插入和删除节点的操作相对高效。然而,它的随机访问效率较低,因为要访问特定位置的元素,必须从头节点开始遍历链表,直到达到目标节点。
特点:
单链表(Singly Linked List)是一种常见的链表数据结构,其主要特点包括以下几点:
-
节点结构:单链表由一组节点组成,每个节点包含两个部分信息:
- 数据域:用于存储实际的元素值。
- 指针域(或引用域):指向下一个节点的引用或指针。
-
单向连接:单链表的节点之间通过指针(引用)实现单向连接。每个节点指向链表中的下一个节点,而最后一个节点的指针通常为空(null或None),表示链表的结束。
-
头节点:单链表通常有一个头节点(Head),它是链表的第一个节点。
-
随机访问效率低:要访问单链表中的特定位置的元素,通常需要从头节点开始遍历链表,直到达到目标位置。因此,单链表的随机访问效率相对较低,时间复杂度为 O(n),其中 n 表示链表的长度。
-
插入和删除效率高:单链表对于插入和删除节点的操作相对高效。插入节点时,只需修改相邻节点的指针,不需要移动整个链表。同样,删除节点时也只需修改相邻节点的指针。
-
动态大小:单链表的大小可以动态增长或缩小,因为节点的插入和删除操作是高效的。
-
占用内存相对较少(考虑到数组创建时就已经占用内存空间,链表是加入元素/节点才占用):与数组相比,单链表通常占用较少的内存,因为它不需要预分配连续的内存空间。
-
适用场景:单链表通常在需要频繁插入和删除元素,而不需要频繁随机访问元素的情况下使用。
总之,单链表是一种简单但常见的数据结构,用于存储和操作一系列元素。它的主要优势在于插入和删除操作的高效性,以及动态大小的能力。然而,随机访问效率较低,因此在需要随机访问元素的情况下,数组可能更为合适。选择链表类型应根据特定需求和应用场景来决定。
双链表(Doubly Linked List)
是一种链表数据结构,与单链表相比,它的节点包含两个指针,分别指向前一个节点和后一个节点。这使得双链表在某些操作上比单链表更加灵活,但也更占用内存。
双链表节点的结构如下:
- 数据域:存储实际的元素值。
- 前驱指针(或前向指针):指向前一个节点的引用或指针。
- 后继指针(或后向指针):指向后一个节点的引用或指针。
下面是一个简单的双链表示例:
1 | null < - [Head] < - > [Node1] < - > [Node2] < - > [Node3] < - > ... < - > [LastNode] < - > null |
[Head]
表示头节点。<-
表示前驱指针。<->
表示后继指针。[Node1]
、[Node2]
、[Node3]
表示实际的数据节点。[LastNode]
表示链表的最后一个节点。null
表示空指针,表示链表的结束。
在上面的示例中,双链表的头节点前驱指针为空,尾节点的后继指针都为空。Node1的前驱指针为空,后继指针指向Node2,Node2的前驱指针指向Node1,后继指针指向Node3,以此类推。
双链表相对于单链表具有以下优势:
-
前向和后向遍历:双链表允许从头到尾或从尾到头轻松遍历链表,而不需要重新遍历。
-
插入和删除效率高:插入和删除节点的操作在双链表中通常比在单链表中更高效,因为不需要查找前一个节点,可以直接通过前驱和后继指针执行操作。
-
反向操作:在某些情况下,需要反向操作,双链表可以轻松满足这种需求。
然而,双链表相对于单链表也有一些缺点,其中之一是占用更多的内存,因为每个节点需要存储两个指针。此外,实现和维护双链表可能比单链表更复杂。
总之,双链表是一种灵活的数据结构,适用于需要前向和后向遍历以及高效插入和删除操作的场景。在选择链表类型时,您应该考虑您的需求以及链表的优缺点。
特点:
双链表(Doubly Linked List)是一种链表数据结构,相对于单链表具有以下主要特点:
-
双向连接:每个节点包含两个指针,一个指向前一个节点(前驱节点),一个指向后一个节点(后继节点)。这使得在双链表中可以轻松从前向后或从后向前遍历链表,而不需要重新遍历。
-
头节点和尾节点:通常,双链表有一个头节点和一个尾节点,它们分别用于表示链表的起始和结束。头节点的前驱指针为空,尾节点的后继指针为空,这样可以方便地在链表的两端插入和删除元素。
-
插入和删除效率高:双链表对于插入和删除节点的操作相对高效。插入和删除节点时,只需修改相邻节点的指针,不需要重新遍历链表。
-
随机访问效率较低:虽然双链表允许从前向后和从后向前遍历链表,但对于随机访问特定位置的元素,仍然需要从头节点或尾节点开始遍历,因此随机访问的效率相对较低,时间复杂度为 O(n),其中 n 表示链表的长度。
-
占用内存相对较多:相对于单链表,双链表通常占用更多的内存,因为每个节点需要存储两个指针(前驱和后继指针)。
-
适用场景:双链表通常在需要频繁从前向后和从后向前遍历链表的场景中使用,以及需要在链表两端进行插入和删除操作的情况下使用。
总之,双链表是一种灵活的数据结构,它允许在链表中轻松进行前向和后向遍历,以及高效地进行插入和删除操作。但它相对于单链表占用更多的内存空间,因此在选择链表类型时,应根据特定需求和应用场景来决定。
双链表的最佳实践:
-
初始化:始终在创建双链表时初始化头节点和尾节点,并确保它们的前驱和后继指针都为空。
-
插入节点:插入节点时,更新前一个节点和后一个节点的指针(与当前节点的关系),以确保链表的正确连接。
-
删除节点:删除节点时,更新前一个节点和后一个节点的指针,以跳过要删除的节点。
-
前向和后向遍历:利用前驱和后继指针进行前向和后向遍历链表。注意在进行遍历之前检查指针是否为空,以防止访问不存在的节点。
-
注意边界情况:在操作头节点和尾节点时要格外小心,因为它们的前驱和后继指针可能为空。
循环链表(特殊的单链表)
循环链表(Circular Linked List)是一种链表数据结构,它与常规链表(单链表或双链表)不同之处在于,循环链表的最后一个节点指向链表的第一个节点,形成一个循环。这个特点使得在循环链表中可以轻松地从任何节点开始遍历整个链表。
以下是循环链表的主要特点和详细解释:
-
循环连接:循环链表的最后一个节点不是指向空(null或nil),而是指向链表的第一个节点,从而形成一个环。这种循环结构使得可以无限循环地遍历链表。
-
头节点:循环链表通常有一个头节点(Head),它是链表的第一个节点。头节点不包含实际数据,只是用于表示链表的起始位置。头节点的后继指针指向链表的第一个实际节点,而尾节点的后继指针指向头节点。
-
插入和删除效率高:与常规链表一样,循环链表对于插入和删除节点的操作相对高效。插入和删除节点时,只需修改相邻节点的指针,不需要重新遍历链表。
-
随机访问效率较低:与常规链表一样,循环链表的随机访问效率较低,因为要访问特定位置的元素,通常需要从头节点开始遍历链表。
-
应用场景:循环链表在某些特定场景中非常有用,例如循环队列(Circular Queue)和循环缓冲区(Circular Buffer)的实现。在这些场景中,需要循环使用有限的存储空间,而循环链表可以很好地满足这种需求。
特点:
-
循环连接:循环链表的最后一个节点不是指向空,而是指向链表的第一个节点,形成一个环。这使得可以轻松地在循环链表中进行循环遍历,而不需要重新遍历整个链表。
-
头节点:循环链表通常有一个头节点(Head),它是链表的第一个节点。头节点的后继指针指向链表的第二节点,而尾节点的后继指针也指向头节点,形成了循环。
-
插入和删除效率高:与常规链表一样,循环链表对于插入和删除节点的操作相对高效。插入和删除节点时,只需修改相邻节点的指针,不需要重新遍历链表。
-
随机访问效率较低:与常规链表一样,循环链表的随机访问效率相对较低,因为要访问特定位置的元素,通常需要从头节点开始遍历链表。
-
应用场景:循环链表在某些特定场景中非常有用,例如实现循环队列(Circular Queue)和循环缓冲区(Circular Buffer)。在这些场景中,需要循环使用有限的存储空间,而循环链表可以很好地满足这种需求。
-
无限循环:由于循环链表的特性,它可以被无限循环遍历,从而适用于需要连续循环访问数据的情况。
总之,循环链表是一种具有循环连接的链表数据结构,使得在链表中可以轻松进行循环遍历。它通常用于需要循环使用数据集的场景,同时具有与常规链表相似的插入和删除操作效率。选择循环链表还是常规链表应根据特定需求和应用场景来决定。
链表的优势
链表相对于数组具有一些优势:
-
动态大小:链表可以根据需要动态增长或缩小,而数组的大小是固定的。
-
插入和删除:在链表中插入或删除元素的操作通常比在数组中更高效,因为不需要移动其他元素。
-
没有预分配内存:链表不需要预分配连续内存空间,这在处理大型数据集时非常有用。
链表的最佳实践
-
选择正确的类型:选择链表类型(单链表、双链表、循环链表等)根据您的需求。不同类型的链表适用于不同的场景。
-
维护头和尾:通常,保持对链表头和尾的引用可以加速插入和删除操作。
-
避免过度使用链表:链表不适用于所有情况。在需要随机访问元素的情况下,数组通常更适合。
Javascript中的链表
单链表示例:
1 // 定义一个节点类:一个节点2个域,数据域、指针域 2 class Node { 3 constructor(data) { 4 this.data = data; // 为数据域赋值 5 this.next = null; // 指针域默认是null,等入链时根据其位置设值:1. 作为head节点,其指针域值为之前的head值。 2. 作为tail节点,其指针域值就是null。 6 } 7 } 8 9 class LinkedList { 10 constructor() { 11 this.head = null; 12 } 13 14 append(data) { 15 const newNode = new Node(data); 16 if (!this.head) { 17 this.head = newNode; 18 } else { 19 let current = this.head; 20 while (current.next) { 21 current = current.next; 22 } 23 current.next = newNode; 24 } 25 } 26 27 display() { 28 let current = this.head; 29 while (current) { 30 console.log(current.data); 31 current = current.next; 32 } 33 } 34 } 35 36 const myList = new LinkedList(); 37 myList.append(1); 38 myList.append(2); 39 myList.append(3); 40 41 myList.display(); // 输出 1, 2, 3
输出:
1 2 3 | 1 2 3 |
双链表示例:
1 class Node { 2 constructor(data) { 3 this.data = data; 4 this.prev = null; // 前驱指针 5 this.next = null; // 后继指针 6 } 7 } 8 9 class DoublyLinkedList { 10 constructor() { 11 this.head = null; 12 this.tail = null; 13 } 14 15 // 在链表尾部添加节点 16 append(data) { 17 const newNode = new Node(data); 18 if (!this.head) { // 如果是空链,待插入的节点即是头节点,也是尾节点 19 this.head = newNode; 20 this.tail = newNode; 21 } else { 22 newNode.prev = this.tail; // 设置待插入的节点前驱指针指向尾节点 23 this.tail.next = newNode; // 当前尾节点的后继指针直线待插入节点 24 this.tail = newNode; // 将待插入节点设置为尾节点即tail变量的值为待插入节点 25 } 26 } 27 28 // 在链表头部添加节点 29 prepend(data) { 30 const newNode = new Node(data); 31 if (!this.head) { // 空链,待插入节点既是头节点也是尾节点 32 this.head = newNode; 33 this.tail = newNode; 34 } else { 35 newNode.next = this.head; // 1. 待插入节点的后继指针,指向当前头节点 36 this.head.prev = newNode; // 2. 当前头节点的前驱指针指向待插入节点 37 this.head = newNode; // 3. 更新head变量的值为待插入节点 38 } 39 } 40 41 // 删除指定节点 42 delete(data) { 43 let current = this.head; 44 while (current) { 45 if (current.data === data) { // 1. 遍历,找到待删除的节点即current节点 46 if (current === this.head) { // 2. 如果是头节点,头节点下一个节点为head变量的值 47 this.head = current.next; 48 if (this.head) { // 2.1 如果头节点不为空,则其前驱节点为null 49 this.head.prev = null; 50 } 51 } else if (current === this.tail) { // 3. 如果待删除的是尾节点 52 this.tail = current.prev; // 3.1 其前驱节点为尾节点 53 this.tail.next = null; // 3.2 要满足尾节点的特点,后继指针为null 54 } else { 55 current.prev.next = current.next; // 4. 非头非尾节点,当前节点的下一个节点作为当前节点上一个节点的后继节点 56 current.next.prev = current.prev; // 4.1 当前节点前驱节点作为当前节点下一个节点的前驱节点 57 } 58 return; 59 } 60 current = current.next; 61 } 62 } 63 64 // 打印链表元素 65 display() { 66 let current = this.head; 67 while (current) { 68 console.log(current.data); 69 current = current.next; 70 } 71 } 72 } 73 74 // 创建双链表实例 75 const myList = new DoublyLinkedList(); 76 77 // 添加元素 78 myList.append(1); 79 myList.append(2); 80 myList.append(3); 81 myList.prepend(0); 82 83 // 打印链表 84 myList.display(); // 输出: 0 1 2 3 85 86 // 删除元素 87 myList.delete(2); 88 89 // 打印链表 90 myList.display(); // 输出: 0 1 3
输出:
1 2 3 4 5 6 7 | 0 1 2 3 0 1 3 |
循环链表示例
1 // 节点类,就是单向链表,把尾节点的后继指针指向了头节点 2 class Node { 3 constructor(data) { 4 this.data = data; 5 this.next = null; // 单向循环链表,有一个后继节点就ok了 6 } 7 } 8 9 class CircularLinkedList { 10 constructor() { 11 this.head = null; // 循环链表,要记录头,方便后面遍历、添加、删除等操作 12 } 13 14 // 在链表尾部添加节点 15 append(data) { 16 const newNode = new Node(data); 17 if (!this.head) { // 1. 如果是空链,那么当前节点就是头节点,也是尾节点,因此其后继指针指向head 18 this.head = newNode; 19 newNode.next = this.head; // 将新节点的下一个指向自身,形成循环 20 } else { // 2. 如果不是空链,从头遍历遍历找到尾节点 21 let current = this.head; 22 while (current.next !== this.head) { // 2.1 尾节点的特点就是其后继指针指向了头节点head 23 current = current.next; 24 } 25 current.next = newNode; // 3. current代表尾节点,其后继节点变成了新节点 26 newNode.next = this.head; // 4. 新节点的下一个指向头节点,形成循环 27 } 28 } 29 30 // 删除指定节点 31 delete(data) { 32 if (!this.head) { // 1. 空节点直接返回 33 return; 34 } 35 let current = this.head; // 2. 从头开始遍历,找目标节点 36 let prev = null; // 目标节点前一个节点 37 38 // 寻找要删除的节点并找到其前驱节点 39 do { 40 if (current.data === data) { // 找到目标节点 41 if (prev) { // 目标节点前一个节点,如果不为空,其后继指针指向目标节点后继指针指向的节点 42 prev.next = current.next; 43 } else { 44 // 如果要删除的是头节点,需要更新头节点 45 let temp = current; 46 while (temp.next !== this.head) { //如果当前节点不是尾节点,就遍历,让temp变成尾节点 47 temp = temp.next; 48 } 49 this.head = current.next; 50 temp.next = this.head; 51 } 52 return; 53 } 54 prev = current; 55 current = current.next; 56 } while (current !== this.head); // 遍历链表 57 } 58 59 // 打印链表元素 60 display() { 61 if (!this.head) { 62 return; 63 } 64 let current = this.head; 65 do { 66 console.log(current.data); 67 current = current.next; 68 } while (current !== this.head); 69 } 70 } 71 72 // 创建循环链表实例 73 const myList = new CircularLinkedList(); 74 75 // 添加元素 76 myList.append(1); 77 myList.append(2); 78 myList.append(3); 79 80 // 打印链表 81 myList.display(); // 输出: 1 2 3 1 2 3 ... 82 83 // 删除元素 84 myList.delete(2); 85 86 // 打印链表 87 myList.display(); // 输出: 1 3 1 3 ...
输出:
1 2 3 4 5 | 1 2 3 1 3 |
Java语言中的链表
单链表示例
1 class Node { 2 int data; 3 Node next; 4 5 Node(int data) { 6 this.data = data; 7 this.next = null; 8 } 9 } 10 11 public class LinkedList { 12 13 private Node head; 14 15 LinkedList() { 16 this.head = null; 17 } 18 19 // 在链表头部插入元素 20 public void prepend(int data) { 21 Node newNode = new Node(data); 22 newNode.next = head; 23 head = newNode; 24 } 25 26 // 在链表尾部追加元素 27 public void append(int data) { 28 Node newNode = new Node(data); 29 if (head == null) { 30 head = newNode; 31 return; 32 } 33 Node current = head; 34 while (current.next != null) { 35 current = current.next; 36 } 37 current.next = newNode; 38 } 39 40 // 删除指定元素 41 public void delete(int data) { 42 if (head == null) { 43 return; 44 } 45 if (head.data == data) { 46 head = head.next; 47 return; 48 } 49 Node current = head; 50 while (current.next != null && current.next.data != data) { 51 current = current.next; 52 } 53 if (current.next != null) { 54 current.next = current.next.next; 55 } 56 } 57 58 // 打印链表元素 59 public void display() { 60 Node current = head; 61 while (current != null) { 62 System.out.print(current.data + " "); 63 current = current.next; 64 } 65 System.out.println(); 66 } 67 68 public static void main(String[] args) { 69 LinkedList myList = new LinkedList(); 70 myList.append(1); 71 myList.append(2); 72 myList.append(3); 73 74 myList.display(); // 输出: 1 2 3 75 76 myList.prepend(0); 77 78 myList.display(); // 输出: 0 1 2 3 79 80 myList.delete(2); 81 82 myList.display(); // 输出: 0 1 3 83 } 84 85 }
输出:
1 2 3 | 1 2 3 0 1 2 3 0 1 3 |
双向链表示例
1 //1. 节点类 2 class Node { 3 int data; 4 Node prev; 5 Node next; 6 7 Node(int data) { 8 this.data = data; 9 this.prev = null; 10 this.next = null; 11 } 12 } 13 14 public class DoublyLinkedList { 15 16 Node head; 17 Node tail; 18 19 // 在链表尾部添加节点 20 public void append(int data) { 21 Node newNode = new Node(data); // 创建节点 22 if (head == null) { // 是空链,新节点即为头节点也是尾节点 23 head = newNode; 24 tail = newNode; 25 } else { 26 newNode.prev = tail; // 当前尾节点作为新节点的前驱节点 27 tail.next = newNode; // 新节点为当前尾节点的后继节点 28 tail = newNode; // 新节点为尾节点 29 } 30 } 31 32 // 在链表头部添加节点 33 public void prepend(int data) { 34 Node newNode = new Node(data); // 1. 创建新节点 35 if (head == null) { // 2. 如果链表为空,新节点既是头也是尾节点 36 head = newNode; 37 tail = newNode; 38 } else { 39 newNode.next = head; // 3. 新节点的后继指针指向头节点 40 head.prev = newNode; // 4. 新节点作为当前头节点的后继节点 41 head = newNode; // 5. 把新节点赋值给头节点变量 42 } 43 } 44 45 // 删除指定节点 46 public void delete(int data) { 47 Node current = head; // 从头开始遍历,找到待删除节点 current 48 while (current != null) { 49 if (current.data == data) { 50 if (current == head) { // 1. 如果待删除节点是头节点 51 head = current.next; // 其下一个节点为头节点 52 if (head != null) { // 如果不为空,则其前驱节点为null 53 head.prev = null; 54 } 55 } else if (current == tail) { // 1. 如果待删除节点是尾节点 56 tail = current.prev; // 当前节点的前驱节点指向tail遍历 57 tail.next = null; // tail的后继指针指向null 58 } else { 59 current.prev.next = current.next; // 非头非尾 60 current.next.prev = current.prev; 61 } 62 return; 63 } 64 current = current.next; 65 } 66 } 67 68 // 打印链表元素 69 public void display() { 70 Node current = head; 71 while (current != null) { 72 System.out.print(current.data + " "); 73 current = current.next; 74 } 75 System.out.println(); 76 } 77 78 public static void main(String[] args) { 79 DoublyLinkedList myList = new DoublyLinkedList(); 80 myList.append(1); 81 myList.append(2); 82 myList.append(3); 83 84 myList.display(); // 输出: 1 2 3 85 86 myList.prepend(0); 87 88 myList.display(); // 输出: 0 1 2 3 89 90 myList.delete(2); 91 92 myList.display(); // 输出: 0 1 3 93 } 94 95 }
输出:
1 2 3 | 1 2 3 0 1 2 3 0 1 3 |
循环链表示例
package org.allen.data.structure.linkedlist; /** * 节点类 */ class Node { int data; // 数据域,用于存放数据 Node next; // 指针域,用于指向后继节点 public Node(int data) { // 生成器快捷键Alt + insert this.data = data; this.next = null; } } /** * 循环链表(单链表的特例) */ public class CircularLinkedList { private Node head; // 头节点,必须要有,后面操作必须用到 // private Node tail; // 尾节点,有在尾部经常添加删除操作的需要 public CircularLinkedList() { this.head = null; } /** * 在链表尾巴添加元素 * * @param data 节点的数据域的值 */ public void append(int data) { // 1. 根据数据域的值构建一个节点对象 Node newNode = new Node(data); // 2. 判断是否空链 if (head == null) { head = newNode; // 2.1 空链,就把待插入节点作为head head.next = head; // 2.1 循环链特殊场景:只有head,head的后继节点执行自己 } else { // 2.2 不是空链(没有保留尾节点),需要从头遍历,找到尾节点,然后建立尾节点与待插入元素的关系 Node current = head; while (current.next != head) { // 尾节点的特点就是后继节点是head current = current.next; // 切换当前节点,直到当前节点current是尾节点 } // 2.2 建立尾节点与待插入节点的关系:当前尾节点下一个节点是待插入节点 current.next = newNode; // 建立循环 newNode.next = head; } } /** * 打印链表元素 */ public void display() { // 1. 如果链表是空链,直接结束 if (head == null) { return; } else { // 打印 Node current = head; do { System.out.println(current.data + "\t"); current = current.next; } while (current.next != head); System.out.println(current.data); // 打印尾节点的值 } } public static void main(String[] args) { CircularLinkedList myList = new CircularLinkedList(); myList.append(1); myList.append(2); myList.append(3); myList.display(); } }
输出:
1 2 3 | 1 2 3 |
Python中的链表
单链表示例
1 class Node: 2 def __init__(self, data): 3 self.data = data 4 self.next = None 5 6 7 class LinkedList: 8 def __init__(self): 9 self.head = None # 要时刻想着维护头节点 10 11 # 在链表头部插入元素 12 def prepend(self, data): # 1. 新节点的指针指向头节点 && 把新节点作为头节点 13 new_node = Node(data) 14 new_node.next = self.head # 新节点的指针指向头节点 15 self.head = new_node # 把新节点作为头节点 16 17 # 在链表尾部追加元素 18 def append(self, data): 19 new_node = Node(data) 20 if not self.head: # 1. 如果是空链,新节点作为头节点 21 self.head = new_node 22 return 23 current = self.head # 2. 找到尾节点,指定其指针指向新节点 24 while current.next: 25 current = current.next 26 current.next = new_node # 此时current为尾节点 27 28 # 删除指定元素 29 def delete(self, data): 30 if not self.head: 31 return 32 if self.head.data == data: # 如果删除的数据是头节点,直接把头节点的下一个节点作为头节点即可 33 self.head = self.head.next 34 return 35 current = self.head # 从头遍历(头节点为删除节点已考虑),从current.next排查、比较 36 while current.next and current.next.data != data: # 找到current.next即为要删除的节点,current表示要删除节点前一节点 37 current = current.next 38 if current.next: # 如果删除的节点不是None 39 current.next = current.next.next # 当前节点的指针( current.next)要指向被删除节点( current.next)的下一个节点(current.next.next) 40 41 # 打印链表元素 42 def display(self): 43 current = self.head 44 while current: 45 print(current.data, end=" ") 46 current = current.next 47 print() 48 49 50 # 创建单链表实例 51 my_list = LinkedList() 52 53 # 添加元素 54 my_list.append(1) 55 my_list.append(2) 56 my_list.append(3) 57 58 # 打印链表 59 my_list.display() # 输出: 1 2 3 60 61 # 在头部插入元素 62 my_list.prepend(0) 63 64 # 打印链表 65 my_list.display() # 输出: 0 1 2 3 66 67 # 删除元素 68 my_list.delete(2) 69 70 # 打印链表 71 my_list.display() # 输出: 0 1 3
输出:
1 2 3 | 1 2 3 0 1 2 3 0 1 3 |
双链表示例
1 ''' 2 双链表时的最佳实践: 3 1. 初始化:始终在创建双链表时初始化头节点和尾节点,并确保它们的前驱和后继引用都为空。 4 2. 插入节点:插入节点时,更新前一个节点和后一个节点的引用,以确保链表的正确连接。 5 3. 删除节点:删除节点时,更新前一个节点和后一个节点的引用,以跳过要删除的节点。 6 4. 前向和后向遍历:利用前驱和后继引用进行前向和后向遍历链表。在遍历之前检查引用是否为空,以防止访问不存在的节点。 7 5. 注意边界情况:在操作头节点和尾节点时要格外小心,因为它们的前驱和后继引用可能为空。 8 ''' 9 10 11 # 1. 定义节点:要维护当前节点的前驱、后继指针 12 class Node: 13 def __init__(self, data): 14 self.data = data 15 self.prev = None 16 self.next = None 17 18 19 # 2. 链表: 要维护头尾2个节点的引用 20 class DoublyLinkedList: 21 def __init__(self): 22 self.head = None 23 self.tail = None 24 25 # 在链表尾部添加节点 26 def append(self, data): 27 new_node = Node(data) 28 if not self.head: # 如果为空链,新节点既是头节点也是尾节点 29 self.head = new_node 30 self.tail = new_node 31 else: # 如果不是空链,直接在尾节点上操作: 1. 尾节点的后继指针指向新节点,新节点的前驱指针指向尾节点。2. 更新新节点为尾节点 32 new_node.prev = self.tail # 1. 尾节点的后继指针指向新节点,新节点的前驱指针指向尾节点 33 self.tail.next = new_node 34 35 self.tail = new_node # 2. 更新新节点为尾节点 36 37 # 在链表头部添加节点 38 def prepend(self, data): 39 new_node = Node(data) 40 if not self.head: # 如果为空链,新节点既是头节点也是尾节点 41 self.head = new_node 42 self.tail = new_node 43 else: 44 new_node.next = self.head # 新节点后继指针指向头节点 45 self.head.prev = new_node # 新节点作为头节点的前驱指针的引用值 46 self.head = new_node # 新节点作为头节点 47 48 # 删除指定节点 49 def delete(self, data): 50 current = self.head 51 while current: # 从头遍历,找到待删除的节点,有3种情况:删除的是头节点、是尾节点、非头尾节点 52 if current.data == data: 53 if current == self.head: # 1. 删除头节点 54 self.head = current.next # 下一个节点作为头节点 55 if self.head: 56 self.head.prev = None # 双向链表,维护其前驱和后继指针,头节点的前驱指针为None 57 if current == self.tail: # 2. 删除尾节点 58 self.tail = current.prev # 维护尾节点: 尾节点前一个节点作为尾节点 59 if self.tail: # 尾节点的特点: 其后继指针为None 60 self.tail.next = None 61 if current.prev: # 删除非头尾节点,为删除节点的前1个节点不为None(不是头节点),则其后继指针指向,删除节点的后继指向的节点 62 current.prev.next = current.next 63 if current.next: # 如果删除节点后继指针不为None(不是尾节点),则要删除的节点的前驱指针指向的节点赋值给其下一个节点的前驱指针 64 current.next.prev = current.prev 65 return 66 current = current.next 67 68 # 打印链表元素 69 def display(self): 70 current = self.head 71 while current: 72 print(current.data, end=" ") 73 current = current.next 74 print() 75 76 77 # 创建双链表实例 78 my_list = DoublyLinkedList() 79 80 # 添加元素 81 my_list.append(1) 82 my_list.append(2) 83 my_list.append(3) 84 85 # 打印链表 86 my_list.display() # 输出: 1 2 3 87 88 # 在头部插入元素 89 my_list.prepend(0) 90 91 # 打印链表 92 my_list.display() # 输出: 0 1 2 3 93 94 # 删除元素 95 my_list.delete(2) 96 97 # 打印链表 98 my_list.display() # 输出: 0 1 3
输出:
1 2 3 | 1 2 3 0 1 2 3 0 1 3 |
循环链表
1 ''' 2 循环链表时的最佳实践: 3 1. 初始化:创建循环链表时,初始化一个头节点,并确保尾节点的 next 引用指向头节点,形成循环。 4 2. 插入节点:插入节点时,更新前一个节点和后一个节点的引用,以确保链表的正确连接。 5 3. 删除节点:删除节点时,更新前一个节点和后一个节点的引用,以跳过要删除的节点。 6 4. 循环遍历:由于循环链表的特性,可以使用一个循环来遍历整个链表,循环条件可以是检查当前节点是否等于头节点。 7 5. 注意边界情况:在操作头节点和尾节点时要格外小心,因为它们的 next 引用可能涉及到循环。 8 ''' 9 10 11 # 1. 节点:需要维护数据域、指针域(指向下一个节点) 12 class Node: 13 def __init__(self, data): 14 self.data = data 15 self.next = None 16 17 18 # 2. 循环链表: 记录头节点。特点尾节点指针指向头节点 19 class CircularLinkedList: 20 def __init__(self): 21 self.head = None 22 23 # 在链表尾部添加节点 24 def append(self, data): 25 new_node = Node(data) 26 if not self.head: # 空链,新节点既是头节点也是尾节点。 27 self.head = new_node 28 new_node.next = self.head # 将新节点的下一个指向自身,形成循环 29 else: 30 current = self.head 31 while current.next != self.head: # 找到尾节点,尾节点的特点就是指针指向头节点 32 current = current.next 33 current.next = new_node # 新节点作为尾节点的指针所指向的节点 34 new_node.next = self.head # 新节点的下一个指向头节点,形成循环 35 36 # 删除指定节点 37 def delete(self, data): 38 if not self.head: 39 return 40 current = self.head 41 prev = None # 当前节点前1个节点 42 43 # 寻找要删除的节点并找到其前驱节点 44 while current.data != data: 45 prev = current 46 current = current.next 47 if current == self.head: # 说明已经遍历了一遍了,没有找到就结束 48 return # 节点不存在 49 50 if prev: # 目标节点前一个节点,不为None 51 prev.next = current.next 52 else: 53 # 如果要删除的是头节点,需要更新头节点 54 temp = current 55 while temp.next != self.head: 56 temp = temp.next 57 self.head = current.next 58 temp.next = self.head 59 60 # 打印链表元素 61 def display(self): 62 if not self.head: 63 return 64 current = self.head 65 while True: 66 print(current.data, end=" ") 67 current = current.next 68 if current == self.head: 69 break 70 print() 71 72 73 # 创建循环链表实例 74 my_list = CircularLinkedList() 75 76 # 添加元素 77 my_list.append(1) 78 my_list.append(2) 79 my_list.append(3) 80 81 # 打印链表 82 my_list.display() # 输出: 1 2 3 83 84 # 删除元素 85 my_list.delete(2) 86 87 # 打印链表 88 my_list.display() # 输出: 1 3
输出:
1 2 | 1 2 3 1 3 |
应用
1. 多项式
class Node: def __init__(self, coefficient, exponent): self.coefficient = coefficient # 系数 self.exponent = exponent # 指数 self.next = None # 下一个节点的引用 class Polynomial: def __init__(self): self.head = None # 多项式链表的头节点 def add_term(self, coefficient, exponent): new_term = Node(coefficient, exponent) if not self.head: self.head = new_term else: current = self.head prev = None while current and current.exponent > exponent: prev = current current = current.next if current and current.exponent == exponent: current.coefficient += coefficient else: new_term.next = current if prev: prev.next = new_term else: self.head = new_term def evaluate(self, x): result = 0 current = self.head while current: result += current.coefficient * (x ** current.exponent) current = current.next return result def display(self): current = self.head while current: print(f"{current.coefficient}x^{current.exponent}", end=" ") current = current.next print() # 创建多项式 P(x) = 3x^4 + 2x^2 + 5 p = Polynomial() p.add_term(3, 4) p.add_term(2, 2) p.add_term(5, 0) # 显示多项式 p.display() # 计算多项式在 x=2 处的值 x_value = 2 result = p.evaluate(x_value) print(f"P({x_value}) = {result}")
输出:
1 2 | 3x ^ 4 2x ^ 2 5x ^ 0 P( 2 ) = 61 |
1 ''' 2 节点类: 3 1. 链表的基本组成部分 4 2. 每个节点都存储了多项式的系数、指数、后继节点的引用 5 ''' 6 class Node: 7 def __init__(self, coefficient, exponent): 8 self.coefficient = coefficient # 多项式某个指数的系数 9 self.exponent = exponent # 多项式某个指数的指数 10 self.next = None # 后继节点的引用,初始None,入链根据情况设值 11 12 13 class Polynomial: 14 def __init__(self): 15 self.head = Node(None, None) # 此处设置头节点不存储数据 16 17 def insert_term(self, coefficient, exponent): 18 new_node = Node(coefficient, exponent) 19 current = self.head 20 while current.next and current.next.exponent > exponent: # 1. 找到插入的位置 21 current = current.next 22 if current.next and current.next.exponent == exponent: # 2. 判断指数是否相同,相同系数相加 23 current.next.coefficient += coefficient 24 else: # 3. 不存在的指数,加入即可 25 new_node.next = current.next 26 current.next = new_node 27 28 def display(self): 29 current = self.head.next 30 while current: 31 print(f"{current.coefficient}x^{current.exponent}", end="") 32 current = current.next 33 if current: 34 print(" + ", end="") 35 print() 36 37 def evaluate(self, x): 38 result = 0 39 current = self.head.next 40 while current: 41 result += current.coefficient * (x ** current.exponent) 42 current = current.next 43 return result 44 45 46 # 创建多项式1: 2x^3 + 3x^2 + 4x^1 + 5x^0 47 poly1 = Polynomial() 48 poly1.insert_term(2, 3) 49 poly1.insert_term(3, 2) 50 poly1.insert_term(4, 1) 51 poly1.insert_term(5, 0) 52 poly1.display() # 输出结果为 2x^3 + 3x^2 + 4x^1 + 5x^0 53 54 # 创建多项式2: 1x^2 + 2x^1 + 3x^0 55 poly2 = Polynomial() 56 poly2.insert_term(1, 2) 57 poly2.insert_term(2, 1) 58 poly2.insert_term(3, 0) 59 poly2.display() # 输出结果为 1x^2 + 2x^1 + 3x^0 60 61 # 计算多项式相加 62 poly_sum = Polynomial() 63 current1 = poly1.head.next 64 current2 = poly2.head.next 65 66 # 方法1: 67 while current1 and current2: 68 if current1.exponent > current2.exponent: 69 poly_sum.insert_term(current1.coefficient, current1.exponent) 70 current1 = current1.next 71 elif current1.exponent < current2.exponent: 72 poly_sum.insert_term(current2.coefficient, current2.exponent) 73 current2 = current2.next 74 else: 75 poly_sum.insert_term(current1.coefficient + current2.coefficient, current1.exponent) 76 current1 = current1.next 77 current2 = current2.next 78 79 # 方法2: 80 # while current1: 81 # poly_sum.insert_term(current1.coefficient, current1.exponent) 82 # current1 = current1.next 83 # while current2: 84 # poly_sum.insert_term(current2.coefficient, current2.exponent) 85 # current2 = current2.next 86 poly_sum.display() # 输出结果为 2x^3 + 4x^2 + 6x^1 + 8x^0 87 88 print(poly_sum.evaluate(1)) # 20 89 print(poly_sum.evaluate(6)) # 620
输出:
1 2 3 4 | 1x ^ 2 + 2x ^ 1 + 3x ^ 0 2x ^ 3 + 4x ^ 2 + 6x ^ 1 + 8x ^ 0 20 620 |
常见问题
1. 在java、python、javascript语言中模拟链表中的节点,该如何声明?
无论你选择使用哪种编程语言,都可以使用这些节点类来创建链表,每个节点都包含一个值和一个指向下一个节点的引用,以构建链表数据结构中的节点。
Java
在Java中,通常使用类来表示链表节点。
1 public class ListNode { 2 int val; 3 ListNode next; 4 5 public ListNode(int val) { 6 this.val = val; 7 this.next = null; 8 } 9 }
这个类具有一个整数值(可以根据需要更改为其他类型)和一个指向下一个节点的引用。
Python
在Python中,你可以使用类来定义链表节点。
class ListNode: def __init__(self, val): self.val = val self.next = None
这个类也有一个值和一个指向下一个节点的引用。
Javascript
在JavaScript中,你可以使用类来表示链表节点。
class ListNode { constructor(val) { this.val = val; this.next = null; } }
这个类具有一个值和一个指向下一个节点的引用。
2. 链表在稀疏矩阵上的应用
稀疏矩阵是指矩阵中大部分元素为零的矩阵。为了有效地存储和处理稀疏矩阵,可以使用链表表示。链表表示的基本思想是只存储非零元素的值以及它们的位置信息(行号和列号),而将零元素省略,从而节省存储空间。
链表表示稀疏矩阵的一种常见方式是使用三元组表示法(Triplet Representation),它使用一个链表来存储所有非零元素的值以及它们的行列坐标。以下是链表表示法的示例:
1 2 3 4 5 6 7 8 9 10 | 稀疏矩阵: 0 0 0 0 0 0 0 0 7 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 链表表示: ( 2 , 3 , 7 ) ( 3 , 2 , 2 ) |
上述示例中,链表中的每个元素都是一个三元组,包含了非零元素的值以及它们的行列坐标。比如(2,3,7)其中2,3是其坐标,7代表其值
典型的应用场景和示例:
-
图像处理:图像通常由大量像素组成,其中大多数像素为白色或透明,因此图像可以表示为稀疏矩阵。例如,OCR(光学字符识别)系统中的图像文本识别。
-
自然语言处理:自然语言处理中的词汇统计或文本表示也可以使用稀疏矩阵来表示,其中每个文档可以看作是一个稀疏向量,表示词汇表中每个词的出现频率或TF-IDF权重。
-
网络分析:在社交网络分析或Web链接分析中,可以使用稀疏邻接矩阵来表示不同实体(如用户或网页)之间的连接关系。
-
有限元分析:工程领域中的有限元分析通常涉及大型稀疏矩阵,用于模拟结构、流体或电磁场的行为。
-
推荐系统:推荐系统中的用户-物品评分矩阵通常是稀疏的,因为用户通常只与少数物品有交互,可以使用稀疏矩阵来表示并进行协同过滤等推荐算法。
在这些应用场景中,链表表示法可以有效地减少存储空间的占用,同时也可以提高算法的效率,因为可以只处理非零元素,而忽略零元素。这对于处理大规模稀疏数据非常重要。
图像处理
python语言
1 ''' 2 在图像处理中,稀疏矩阵通常用于表示图像数据,因为图像中大部分像素都是相同的颜色或值,而只有少数像素具有不同的颜色或值。 3 这种情况下,使用稀疏矩阵可以节省内存空间和提高处理效率。Python中的典型示例是使用链表来表示稀疏矩阵。 4 ''' 5 6 7 class Node: # 节点,存储非0元素以及其坐标 8 def __init__(self, row, col, value): 9 self.row = row 10 self.col = col 11 self.value = value 12 self.next = None 13 14 15 class SparseMatrix: 16 def __init__(self, rows, cols): 17 self.rows = rows 18 self.cols = cols 19 self.head = None 20 21 def add_element(self, row, col, value): 22 if row < 0 or row >= self.rows or col < 0 or col >= self.cols: 23 raise ValueError("Invalid row or column index") 24 25 if value != 0: 26 new_node = Node(row, col, value) 27 if self.head is None: 28 self.head = new_node 29 else: 30 current = self.head 31 while current.next: 32 current = current.next 33 current.next = new_node 34 35 def get_element(self, row, col): 36 current = self.head 37 while current: 38 if current.row == row and current.col == col: 39 return current.value 40 current = current.next 41 return 0 42 43 def to_list(self): 44 result = [] 45 for i in range(self.rows): 46 row = [] 47 for j in range(self.cols): 48 row.append(self.get_element(i, j)) 49 result.append(row) 50 return result 51 52 53 # 创建一个3x3的稀疏矩阵 54 matrix = SparseMatrix(3, 3) 55 matrix.add_element(0, 0, 1) 56 matrix.add_element(1, 1, 2) 57 matrix.add_element(2, 2, 3) 58 59 # 打印稀疏矩阵的列表表示 60 print(matrix.to_list()) 61 62 # 获取特定位置的元素值 63 print(matrix.get_element(1, 1))
输出:
1 2 | [[ 1 , 0 , 0 ], [ 0 , 2 , 0 ], [ 0 , 0 , 3 ]] 2 |
Java 语言
1 package org.allen.data.structure.linkedlist; 2 3 /** 4 * 链表类:用链表表示稀疏矩阵 5 */ 6 class SparseMatrixNode { // 一个节点,可以看出2个节点,行列节点,每次都要维护其下一个行、下一个列节点 7 int row, col, value; 8 SparseMatrixNode nextRow, nextCol; 9 10 public SparseMatrixNode(int row, int col, int value) { 11 this.row = row; 12 this.col = col; 13 this.value = value; 14 this.nextRow = null; 15 this.nextCol = null; 16 } 17 } 18 19 class SparseMatrix { 20 int numRows; 21 int numCols; 22 SparseMatrixNode[] rowHeads; 23 SparseMatrixNode[] colHeads; 24 25 public SparseMatrix(int numRows, int numCols) { 26 this.numRows = numRows; // 行数对应矩阵n 27 this.numCols = numCols; // 列数对应矩阵m 28 rowHeads = new SparseMatrixNode[numRows]; 29 colHeads = new SparseMatrixNode[numCols]; 30 for (int i = 0; i < numRows; i++) { 31 rowHeads[i] = new SparseMatrixNode(i, -1, 0); 32 } 33 for (int j = 0; j < numCols; j++) { 34 colHeads[j] = new SparseMatrixNode(-1, j, 0); 35 } 36 } 37 38 public void insert(int row, int col, int value) { 39 if (row < 0 || row >= numRows || col < 0 || col >= numCols) { 40 throw new IllegalArgumentException("Invalid row or column index"); 41 } 42 SparseMatrixNode newNode = new SparseMatrixNode(row, col, value); 43 SparseMatrixNode currentRowNode = rowHeads[row]; 44 SparseMatrixNode currentColNode = colHeads[col]; 45 46 // Insert into the row list: 维护列链表 47 while (currentRowNode.nextRow != null && currentRowNode.nextRow.col < col) { 48 currentRowNode = currentRowNode.nextRow; 49 } 50 newNode.nextRow = currentRowNode.nextRow; 51 currentRowNode.nextRow = newNode; 52 53 // Insert into the column list 维护行列表 54 while (currentColNode.nextCol != null && currentColNode.nextCol.row < row) { 55 currentColNode = currentColNode.nextCol; 56 } 57 newNode.nextCol = currentColNode.nextCol; 58 currentColNode.nextCol = newNode; 59 } 60 61 public void print() { 62 for (int i = 0; i < numRows; i++) { 63 SparseMatrixNode current = rowHeads[i].nextRow; 64 for (int j = 0; j < numCols; j++) { 65 if (current != null && current.col == j) { 66 System.out.print(current.value + " "); 67 current = current.nextRow; 68 } else { 69 System.out.print("0 "); 70 } 71 } 72 System.out.println(); 73 } 74 } 75 } 76 77 public class Main { 78 public static void main(String[] args) { 79 SparseMatrix matrix = new SparseMatrix(4, 5); 80 matrix.insert(0, 0, 1); 81 matrix.insert(1, 1, 2); 82 matrix.insert(2, 2, 3); 83 matrix.print(); 84 } 85 }
输出:
1 2 3 4 | 1 0 0 0 0 0 2 0 0 0 0 0 3 0 0 0 0 0 0 0 |
javascript语言:
1 class Node { 2 constructor(row, col, value) { 3 this.row = row; 4 this.col = col; 5 this.value = value; 6 this.next = null; 7 } 8 } 9 10 class SparseMatrix { 11 constructor(rows, cols) { 12 this.rows = rows; 13 this.cols = cols; 14 this.head = new Node(-1, -1, null); // 头节点,为空节点 15 } 16 17 insert(row, col, value) { 18 // 创建一个新节点 19 const newNode = new Node(row, col, value); 20 21 // 在链表中插入节点 22 let current = this.head; 23 while (current.next) { 24 current = current.next; 25 } 26 current.next = newNode; 27 } 28 29 getValue(row, col) { 30 let current = this.head.next; 31 while (current) { 32 if (current.row === row && current.col === col) { 33 return current.value; 34 } 35 current = current.next; 36 } 37 return 0; // 默认值为零 38 } 39 } 40 41 // 创建一个 5x5 的稀疏矩阵 42 const matrix = new SparseMatrix(5, 5); 43 44 // 插入一些非零元素 45 matrix.insert(1, 1, 5); 46 matrix.insert(2, 2, 8); 47 matrix.insert(3, 3, 6); 48 49 // 执行图像处理操作:将每个像素值减去 2 50 // for (let i = 0; i < 5; i++) { 51 // for (let j = 0; j < 5; j++) { 52 // const value = matrix.getValue(i, j); 53 // matrix.insert(i, j, value - 2); 54 // } 55 // } 56 57 // 打印处理后的稀疏矩阵 58 for (let i = 0; i < 5; i++) { 59 let row = ''; 60 for (let j = 0; j < 5; j++) { 61 row += matrix.getValue(i, j) + ' '; 62 } 63 console.log(row); 64 }
输出:
1 2 3 4 5 | 0 0 0 0 0 0 5 0 0 0 0 0 8 0 0 0 0 0 6 0 0 0 0 0 0 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?