Danh sách liên kết đôi: C++, Python (Ví dụ về mã)

Danh sách liên kết đôi là gì?

Trong danh sách liên kết đôi, mỗi nút có liên kết đến cả nút trước và nút tiếp theo. Mỗi nút bao gồm ba phần tử: một phần tử chứa dữ liệu và hai phần tử còn lại là con trỏ của nút tiếp theo và trước đó. Hai con trỏ này giúp chúng ta tiến hoặc lùi từ một nút cụ thể.

Đây là cấu trúc cơ bản của danh sách liên kết đôi.

Cấu trúc của danh sách liên kết đôi
Cấu trúc của danh sách liên kết đôi

Mỗi danh sách liên kết đều có nút đầu và nút đuôi. Nút Đầu không có nút “trước” (con trỏ trước) và nút đuôi không có nút “tiếp theo”.

Dưới đây là một số thuật ngữ quan trọng đối với danh sách liên kết đôi:

  • Prev: Mỗi nút được liên kết với nút trước đó của nó. Nó được sử dụng như một con trỏ hoặc liên kết.
  • Tiếp theo: Mỗi nút được liên kết với nút tiếp theo của nó. Nó được sử dụng như một con trỏ hoặc liên kết.
  • ngày: Điều này được sử dụng để lưu trữ dữ liệu trong một nút. “Dữ liệu” có thể chứa thông tin khác Cấu trúc dữ liệu bên trong nó. Ví dụ: chuỗi, từ điển, bộ, hashmap, v.v. có thể được lưu trữ trong “Dữ liệu”.

Đây là cấu trúc cơ bản của một nút trong danh sách liên kết đôi:

Cấu trúc của một nút trong Danh sách liên kết đôi

Cấu trúc của một nút trong Danh sách liên kết kép

Operacác chức năng của danh sách liên kết đôi

Các hoạt động của danh sách liên kết đôi bao gồm thêm và xóa các nút, chèn và xóa các nút và duyệt danh sách liên kết từ trên xuống dưới.

Đây là danh sách các thao tác chúng ta có thể thực hiện bằng danh sách liên kết đôi:

  • Chèn ở phía trước
  • Chèn vào đuôi hoặc nút cuối cùng
  • Chèn sau một nút
  • Chèn trước một nút
  • Xóa từ phía trước
  • Xóa từ đuôi
  • Tìm kiếm và xóa một nút
  • Đi ngang từ đầu đến đuôi
  • Xoay đuôi tới đầu

Chúng ta sẽ thấy cách triển khai và mã giả cho tất cả các hoạt động ở trên.

Chèn vào trước danh sách liên kết đôi

Chèn vào phía trước nghĩa là chúng ta cần tạo một nút trong danh sách liên kết và đặt nó vào đầu danh sách liên kết.

Ví dụ: có một nút nhất định “15”. Chúng ta cần thêm phần này vào nút đầu.

Đây là hai điều kiện quan trọng khi thực hiện thao tác này:

  1. Nút mới sẽ là nút đầu nếu không có nút nào trong Danh sách liên kết đôi.
  2. Nếu đã có nút đầu thì nút đầu trước sẽ được thay thế bằng nút mới.

Đây là mã giả cho thao tác này:

function insertAtFront (ListHead, value):
  newNode = Node()
  newNode.value = value
  ListHead.prev = NewNode
  NewNode.next = ListHead
  newNode.prev = NULL
  return ListHead
Chèn vào nút phía trước
Chèn vào nút phía trước

Chèn vào cuối danh sách liên kết đôi

“Insertion at the end” nghĩa là chúng ta sẽ tạo một node trong danh sách liên kết và đặt nó ở cuối.

Để thực hiện điều này, chúng ta có thể sử dụng hai phương pháp:

  • Phương pháp 1: Bắt đầu duyệt từ phần đầu của Danh sách liên kết đôi cho đến khi phần “tiếp theo” trở thành rỗng. Sau đó liên kết nút mới với nút “tiếp theo”.
  • Phương pháp 2: Lấy nút cuối cùng của Danh sách liên kết đôi. Sau đó, nút “tiếp theo” của nút cuối cùng sẽ liên kết với nút mới. Bây giờ, nút mới sẽ là nút đuôi.

Đây là mã giả để chèn vào nút đuôi:

function insertAtTail(ListHead, value):
  newNode = Node()
  newNode.value = value
  newNode.next = NULL
  while ListHead.next is not NULL:
	then ListHead = ListHead.next
  newNode.prev = ListHead
  ListHead.next = newNode
  return ListHead
Chèn vào cuối Danh sách liên kết

Chèn vào cuối danh sách liên kết

Chèn sau một nút

Giả sử chúng ta có một danh sách liên kết kép hiện có như sau:

Chèn sau một nút

Chúng tôi muốn chèn một nút nhất định sẽ được liên kết sau nút có giá trị “12”.

Đây là các bước cho việc này:

Bước 1) Đi từ đầu đến nút cuối cùng. Kiểm tra nút nào có giá trị là “12”.

Bước 2) Tạo một nút mới và gán nó làm con trỏ tiếp theo của nút “12”. Nút “tiếp theo” của nút mới sẽ là 15.

Đây là mã giả để chèn một nút sau một nút trong danh sách liên kết đôi:

function insertAfter(ListHead, searchItem, value):
  List = ListHead
  NewNode = Node()
  NewNode.value = value
  while List.value is not equal searchItem
	then List = ListHead.next
  List = List.next
  NewNode.next = List.next
  NewNode.prev = List
  List.next = NewNode
Chèn sau một nút

Chèn sau một nút

Chèn trước một nút

Thao tác này tương tự như thao tác “chèn sau một nút”. Chúng ta cần tìm kiếm giá trị của một nút cụ thể để thực hiện việc này. Sau đó chúng ta sẽ tạo một nút mới và chèn nó vào trước nút được tìm kiếm.

Giả sử chúng ta muốn chèn một nút đã cho “15” trước nút “12”. Sau đây là các bước để thực hiện:

Bước 1) Duyệt danh sách liên kết từ nút đầu đến nút đuôi.

Bước 2) Kiểm tra xem con trỏ tiếp theo của nút hiện tại có giá trị “12” hay không.

Bước 3) Chèn nút mới làm nút “tiếp theo” của nút hiện tại.

Đây là mã giả để chèn một nút trước một nút trong Danh sách liên kết kép:

function insertBefore(ListHead, searchItem, value):
  List = ListHead
  NewNode = Node()
  NewNode.value = value
  while List.next.value is not equal searchItem
	then List = ListHead.next
  NewNode.next = List.next
  NewNode.prev = List
  List.next = NewNode
Chèn một nút trước một nút

Chèn một nút trước một nút

Xóa phần đầu danh sách liên kết đôi

Nút đầu trong danh sách liên kết đôi không có nút nào trước đó. Vì vậy, con trỏ tiếp theo sẽ là nút đầu mới nếu chúng ta muốn xóa nút đầu. Chúng ta cũng cần giải phóng bộ nhớ bị chiếm bởi một nút trong khi xóa một nút.

Dưới đây là các bước để xóa nút đầu:

Bước 1) Gán một biến cho nút đầu hiện tại.

Bước 2) Truy cập nút “tiếp theo” của nút chính hiện tại và đặt nút “trước” (con trỏ trước) là '' NULL". Điều này có nghĩa là chúng tôi đang ngắt kết nối nút thứ hai khỏi nút đầu tiên.

Bước 3) Giải phóng bộ nhớ bị chiếm bởi nút đầu trước đó.

Đây là mã giả để xóa phần đầu khỏi danh sách liên kết đôi:

function deleteHead(ListHead):
  PrevHead = ListHead
  ListHead = ListHead.next
  ListHead.prev = NULL
  PrevHead.next = NULL
  free memory(PrevHead)
  return ListHead
Xóa nút đầu

Xóa nút đầu

Cần phải xóa bộ nhớ được phân bổ sau khi thực hiện bất kỳ loại thao tác xóa nào. Nếu không, trong toàn bộ thời gian chạy của chương trình, bộ nhớ cho khối đã xóa sẽ vẫn bị chiếm dụng. Không ứng dụng nào khác có thể sử dụng phân đoạn bộ nhớ đó.

Xóa phần đuôi của danh sách liên kết đôi.

Thao tác này gần giống như việc xóa phần đầu. Ở đây, thay vì phần đầu, chúng ta cần xóa phần đuôi. Để xác định một nút là nút đuôi, chúng ta nên kiểm tra xem con trỏ tiếp theo hoặc nút tiếp theo có rỗng hay không. Sau khi xóa phần đuôi, chúng ta cần giải phóng bộ nhớ.

Thao tác này còn được gọi là "xóa từ phía sau".

Dưới đây là các bước để thực hiện việc này:

Bước 1) Duyệt cho đến nút cuối của danh sách liên kết đôi.

Bước 2) Gán một biến hoặc con trỏ tới nút đuôi.

Bước 3) Đặt nút “tiếp theo” thành NULL và giải phóng bộ nhớ của nút đuôi.

Đây là mã giả để xóa nút đuôi:

function DeleteTail( ListHead ):
  head = ListHead
  while ListHead.next is not NULL:
	ListHead = ListHead.next
  Tail = ListHead.next
  ListHead.next = NULL
  free memory( Tail )
  return head

Xóa phần đuôi của liên kết đôi

Tìm kiếm và xóa một nút khỏi Danh sách liên kết đôi

Thao tác này cho phép chúng ta tìm kiếm dữ liệu nút cụ thể và xóa nút đó. Chúng ta cần thực hiện tìm kiếm tuyến tính vì danh sách liên kết là cấu trúc dữ liệu tuyến tính. Sau khi xóa chúng ta cũng cần giải phóng bộ nhớ.

Sau đây là các bước để tìm kiếm và xóa một nút trong danh sách liên kết đôi:

Bước 1) Duyệt danh sách liên kết từ đầu cho đến khi giá trị của nút bằng mục tìm kiếm.

Bước 2) Gán một biến “deleteNode” cho nút phù hợp.

Bước 3) Gán nút trước của “deleteNode” cho nút tiếp theo.

Bước 4) Giải phóng bộ nhớ của “deleteNode”

Sau đây là mã giả để tìm kiếm và xóa một nút khỏi danh sách được liên kết:

function SearchAndDelete(ListHead, searchItem):
  head = ListHead
  while ListHead.next.value not equals searchItem:
	head = head.next
  deleteNode = head.next
  head.next = head.next.next
  head.next.prev = head
  deleteNode.next, deleteNode.next = NULL
  free memory(deleteNode)
  return ListHead
Tìm kiếm và Xóa Operasản xuất

Thao tác tìm kiếm và xóa

Duyệt danh sách liên kết đôi từ phía trước

Để duyệt từ nút đầu và lặp qua nút tiếp theo cho đến khi chúng ta tìm thấy “NULL”. Trong khi duyệt qua từng nút, chúng ta có thể in giá trị của nút đó. Dưới đây là các bước để di chuyển ngang từ phía trước (hướng về phía trước):

Bước 1) Gán một con trỏ hoặc biến cho nút đầu hiện tại.

Bước 2) Lặp lại nút tiếp theo của phần đầu cho đến khi nhận được “NULL”

Bước 3) In dữ liệu của nút trong mỗi lần lặp.

Bước 4) Trả về nút đầu.

Đây là mã giả để duyệt qua Danh sách liên kết đôi từ phía trước:

function traverseFromFront(ListHead):
  head = ListHead
  while head not equals to NULL:
	print head.data
	head = head.next
  return ListHead

Ở đây, việc trả lại là không bắt buộc. Nhưng cách tốt nhất là trả lại nút đầu sau các thao tác.

Duyệt danh sách liên kết đôi từ phía sau

Thao tác này ngược lại với thao tác di chuyển ngang từ phía trước. Cách tiếp cận giống nhau với một chút khác biệt. Chúng ta phải duyệt đến nút cuối và sau đó duyệt từ nút cuối tới nút trước.

Dưới đây là các bước để duyệt qua danh sách liên kết đôi từ phía sau:

Bước 1) Di chuyển cho đến khi chúng ta đến được nút đuôi.

Bước 2) Từ nút đuôi, chúng ta sẽ duyệt qua cho đến khi nhận được nút trước đó là “NULL”. “prev” (con trỏ trước) sẽ rỗng đối với nút đầu

Bước 3) Ở mỗi lần lặp, chúng tôi sẽ in dữ liệu nút.

Đây là mã giả để duyệt từ phía sau:

function traverseFromBack(ListHead):
  head = ListHead
  while head not equals NULL:
	head = head.next
  tail = head
  while tail not equal to NULL:
	print tail.value
	tail = tail.prev
  return ListHead

Sự khác biệt giữa danh sách liên kết đơn và đôi

Sự khác biệt chính giữa danh sách Liên kết đơn và Danh sách liên kết kép là số lượng liên kết.

Sự khác biệt giữa danh sách liên kết đơn và đôi

Đây là sự khác biệt giữa các nút của Danh sách liên kết đơn và cấu trúc nút của Danh sách liên kết đôi:

Phần Danh sách liên kết đơn Danh sách được liên kết gấp đôi
Structure Danh sách liên kết đơn có một trường dữ liệu và một liên kết đến nút tiếp theo. Danh sách liên kết đôi có một trường dữ liệu và hai liên kết. Một cho nút trước và một cho nút tiếp theo.
Truyền tải Nó chỉ có thể đi từ đầu đến đuôi. Nó có thể di chuyển cả về phía trước và phía sau.
Bộ nhớ Chiếm ít bộ nhớ hơn. Nó chiếm nhiều bộ nhớ hơn danh sách liên kết đơn.
Khả Năng Tiếp Cận Danh sách liên kết đơn kém hiệu quả hơn vì chúng chỉ sử dụng một liên kết đến nút tiếp theo. Không có liên kết cho nút trước đó. Danh sách liên kết đôi hiệu quả hơn Danh sách liên kết đơn.

Danh sách liên kết đôi trong C++

#include<iostream>
using namespace std;
  struct node{
  int data;
  struct node *next;
  struct node *prev;
  };
void insertFront(node* &listHead, int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;
  newNode->next = NULL;
  if(listHead!=NULL)
  {
	listHead->prev = newNode;
  	newNode->next = listHead;
  }
  
  listHead = newNode;
  cout<<"Added "<<value<<" at the front"<<endl;
  }
void insertEnd(node * &listHead, int value){
  if(listHead==NULL)
  {
  	insertFront(listHead,value);
  }
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;
  newNode->next = NULL;
  node *head = listHead;
  while(head->next!=NULL){
  head = head->next;
  }
  head->next = newNode;
  newNode->prev = head;
  cout<<"Added "<<value<<" at the end"<<endl;
  }
void insertAfter(node * &listHead, int searchValue,int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;	
  newNode->next = NULL;
  node *head = listHead;	
  while(head->next!=NULL &&  head->data!=searchValue){	
  head = head->next;	
  }	
  newNode->next = head->next;	
  head->next = newNode;	
  newNode->prev = head;	
  if(newNode->next !=NULL){	
  newNode->next->prev = newNode;	
  }	
  cout<<"Inserted "<<value<<" after node "<<searchValue<<endl;
  }
void insertBefore(node * &listHead, int searchValue,int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;	
  newNode->next = NULL;	
  node *head = listHead;	
  while(head->next!=NULL &&  head->next->data!=searchValue){	
  head = head->next;	
  }	
  newNode->next = head->next;	
  head->next = newNode;	
  newNode->prev = head;	
  if(newNode->next !=NULL){	
  newNode->next->prev = newNode;	
  }	
  cout<<"Inserted "<<value<<" before node "<<searchValue<<endl;	
  }
void traverseFromFront(node *listHead){
  node* head = new node();
  head = listHead;
  cout<<"Traversal from head:\t";	
  while(head!=NULL){	
  cout<<head->data<<"\t ";	
  head = head->next;	
  }	
  cout<<endl;
  }	
void traverseFromEnd(node *listHead){
  node* head = new node();
  head = listHead;	
  cout<<"Traversal from head:\t";	
  while(head->next!=NULL){	
  head = head->next;	
  }	
  node *tail = head;	
  while(tail!=NULL){	
  cout<<tail->data<<"\t";	
  tail = tail->prev;	
  }	
  cout<<endl;	
  }
void searchAndDelete(node **listHead, int searchItem){
  node* head = new node();
  head = (*listHead);
  while(head->next!=NULL &&  head->data!=searchItem){
  head = head->next;
  }
  if(*listHead == NULL || head == NULL) return;
  if((*listHead)->data == head->data){
  	*listHead = head->next;
  }
  if(head->next != NULL){
  	head->next->prev = head->prev;
  }
  
  if(head->prev != NULL){
  	head->prev->next = head->next;
  }
  free(head);
  cout<<"Deleted Node\t"<<searchItem<<endl;
  return;
  }
int main(){
  node *head = NULL;
  insertFront(head,5);
  insertFront(head,6);
  insertFront(head,7);
  insertEnd(head,9);
  insertEnd(head,10);
  insertAfter(head,5,11);
  insertBefore(head,5,20);
  traverseFromFront(head);
  traverseFromEnd(head);
  searchAndDelete(&head,7);
  traverseFromFront(head);
  traverseFromEnd(head);
}

Đầu ra

Added 5 at the front
Added 6 at the front
Added 7 at the front
Aded 9 at the end
Added 10 at the end
Inserted 11 after node 5
Inserted 20 before node 5
Traversal from head:    7        6       20      5       11      9       10
Traversal from head:    10      9       11      5       20      6       7
Deleted Node    7
Traversal from head:    6        20      5       11      9       10
Traversal from head:    10      9       11      5       20      6

Danh sách liên kết đôi trong Python

class node:
  def __init__(self,data = None, prev=None, next = None):
    self.data = data
    self.next = next
    self.prev = prev
class DoublyLinkedList:
  def __init__(self):
    self.head = None

  def insertFront(self, val):
    newNode = node(data=val)
    newNode.next = self.head
    if self.head is not None:
      self.head.prev = newNode
    self.head = newNode
    print("Added {} at the front".format(val))

  def insertEnd(self,val):
    newNode = node(data=val)
    if self.head is None:
      self.insertFront(val)
    
    temp = self.head
    while temp.next is not None:
      temp = temp.next
    temp.next = newNode
    newNode.prev = temp
    print("Added {} at the end".format(val))

  def traverseFromFront(self):
    temp = self.head
    print("Traversing from head:\t",end="")
    while temp:
      print("{}\t".format(temp.data),end="")
      temp = temp.next
    print()

  def traverseFromEnd(self):
    temp = self.head
    print("Traversing from Tail:\t",end="")
    while temp.next is not None:
      temp = temp.next
    tail = temp
    while tail is not None:
      print("{}\t".format(tail.data),end="")
      tail = tail.prev
    print()
  
  def insertAfter(self,searchItem, value):
    newNode = node(data=value)

    temp = self.head
    while temp.next is not None and temp.data is not searchItem:
      temp = temp.next
    
    newNode.next = temp.next
    temp.next = newNode
    newNode.prev = temp

    if newNode.next is not None:
      newNode.next.prev = newNode
    print("Inserted {} after node {}".format(value,searchItem))

  def insertBefore(self,searchItem, value):
    newNode = node(data=value)

    temp = self.head
    while temp.next is not None and temp.next.data is not searchItem:
      temp = temp.next
    
    newNode.next = temp.next
    temp.next = newNode
    newNode.prev = temp

    if newNode.next is not None:
      newNode.next.prev = newNode
    print("Inserted {} before node {}".format(value,searchItem))

  def searchAndDelete(self,searchItem):
    temp = self.head

    while temp.next is not None and temp.data is not searchItem:
      temp = temp.next
    
    if self.head is None or temp is None:
      return

    if self.head.data is temp.data:
      self.head = temp.next

    if temp.next is not None:
      temp.next.prev = temp.prev
    
    if temp.prev is not None:
      temp.prev.next = temp.next
    
    print("Deleted Node\t{}".format(searchItem))
    return

doublyLinkedList = DoublyLinkedList()
doublyLinkedList.insertFront(5)
doublyLinkedList.insertFront(6)
doublyLinkedList.insertFront(7)
doublyLinkedList.insertEnd(9)
doublyLinkedList.insertEnd(10)
doublyLinkedList.insertAfter(5, 11)
doublyLinkedList.insertBefore(5, 20)
doublyLinkedList.traverseFromFront()
doublyLinkedList.traverseFromEnd()
doublyLinkedList.searchAndDelete(7)
doublyLinkedList.traverseFromFront()
doublyLinkedList.traverseFromEnd()

Đầu ra

Added 5 at the front
Added 6 at the front
Added 7 at the front
Added 9 at the end
Added 10 at the end
Inserted 11 after node 5
Inserted 20 before node 5
Traversing from head:   7       6       20      5       11      9       10
Traversing from Tail:   10      9       11      5       20      6       7
Deleted Node    7
Traversing from head:   6       20      5       11      9       10
Traversing from Tail:   10      9       11      5       20      6

Độ phức tạp của danh sách liên kết kép

Nhìn chung, độ phức tạp về thời gian được chia thành ba loại.

Đó là:

  1. Trường hợp tốt nhất
  2. Trường hợp trung bình
  3. Trường hợp xấu nhất

Độ phức tạp thời gian trong trường hợp tốt nhất cho Danh sách liên kết kép:

  1. Chèn vào đầu hoặc đuôi sẽ tốn O(1). Bởi vì chúng ta không cần phải duyệt bên trong danh sách được liên kết. Con trỏ đầu và đuôi có thể cho chúng ta quyền truy cập vào nút đầu và nút đuôi với độ phức tạp thời gian O(1).
  2. Xóa ở đầu hoặc đuôi sẽ tốn O(1).
  3. Tìm kiếm một nút sẽ có độ phức tạp thời gian là O(1). Bởi vì nút mục tiêu có thể là nút đầu.

Độ phức tạp thời gian trong trường hợp trung bình của Danh sách liên kết kép:

  1. Việc chèn vào đầu hoặc đuôi sẽ có độ phức tạp về thời gian là O(1).
  2. Việc xóa ở đầu hoặc đuôi sẽ có độ phức tạp thời gian là O(1).
  3. Tìm kiếm một nút sẽ có độ phức tạp thời gian là O(n). Bởi vì mục tìm kiếm có thể nằm ở bất kỳ đâu giữa danh sách được liên kết. Ở đây, “n” là tổng số nút có trong danh sách được liên kết.

Độ phức tạp thời gian trong trường hợp xấu nhất của danh sách liên kết kép sẽ giống với trường hợp trung bình.

Độ phức tạp của bộ nhớ danh sách liên kết đôi

Độ phức tạp của bộ nhớ là O(n), trong đó “n” là tổng số nút. Trong khi triển khai danh sách liên kết, chúng ta phải giải phóng bộ nhớ đã sử dụng. Nếu không, đối với danh sách liên kết lớn hơn, nó sẽ gây ra rò rỉ bộ nhớ.

Tổng kết

  • Danh sách liên kết là một loại cấu trúc dữ liệu tuyến tính, một tập hợp dữ liệu được biểu diễn theo cách tuyến tính.
  • Danh sách liên kết đôi là một loại danh sách liên kết trong đó một nút có liên kết với cả nút trước và nút tiếp theo.
  • Danh sách liên kết đôi chứa tất cả các thao tác như thêm nút, xóa nút, chèn nút sau hoặc trước nút khác và duyệt qua danh sách liên kết từ đầu đến cuối.
  • Danh sách liên kết đôi có một trường dữ liệu và hai liên kết. Một cho nút trước và một cho nút tiếp theo.
  • Độ phức tạp của danh sách liên kết kép Trường hợp tốt nhất, Trường hợp trung bình
  • Và trường hợp xấu nhất.