รายการที่เชื่อมโยงเดี่ยวในโครงสร้างข้อมูล

รายการเชื่อมโยงเดี่ยวคืออะไร?

Singly Linked List เป็นโครงสร้างข้อมูลเชิงเส้นและเป็นทิศทางเดียว โดยที่ข้อมูลจะถูกบันทึกไว้บนโหนด และแต่ละโหนดจะเชื่อมต่อกันผ่านลิงก์ไปยังโหนดถัดไป แต่ละโหนดประกอบด้วยช่องข้อมูลและลิงก์ไปยังโหนดถัดไป รายการที่เชื่อมโยงแบบเดี่ยวสามารถสำรวจได้ในทิศทางเดียวเท่านั้น ในขณะที่รายการที่เชื่อมโยงแบบทวีคูณสามารถสำรวจได้ทั้งสองทิศทาง

นี่คือโครงสร้างโหนดของรายการเชื่อมโยงเดี่ยว:

โครงสร้างของโหนดในรายการที่เชื่อมโยง
โครงสร้างของโหนดในรายการที่เชื่อมโยง

เหตุใดจึงใช้รายการที่เชื่อมโยงมากกว่าอาร์เรย์

มีหลายกรณีที่ควรใช้รายการที่เชื่อมโยงมากกว่า แถว- ต่อไปนี้เป็นบางสถานการณ์:

  • ไม่ทราบจำนวนองค์ประกอบ: เมื่อคุณไม่ทราบว่าต้องจัดเก็บองค์ประกอบจำนวนเท่าใดในระหว่างรันไทม์ของโปรแกรม คุณสามารถใช้รายการที่เชื่อมโยงได้ หน่วยความจำจะถูกจัดสรรแบบไดนามิกเมื่อคุณเพิ่มองค์ประกอบลงในรายการที่เชื่อมโยง
  • การเข้าถึงแบบสุ่ม: ในสถานการณ์ที่คุณไม่จำเป็นต้องใช้การเข้าถึงแบบสุ่มจากองค์ประกอบ คุณสามารถใช้รายการที่เชื่อมโยงได้
  • การแทรกตรงกลาง: การแทรกตรงกลางอาร์เรย์เป็นงานที่ซับซ้อน เนื่องจากคุณต้องดันองค์ประกอบอื่น ๆ ไปทางขวา อย่างไรก็ตาม รายการที่เชื่อมโยงช่วยให้คุณสามารถเพิ่มองค์ประกอบลงในตำแหน่งใดก็ได้ที่คุณต้องการ

Operaของรายการลิงค์เดี่ยว

Singly Linked List เหมาะกับการจัดสรรหน่วยความจำแบบไดนามิก โดยรองรับการทำงานพื้นฐานทั้งหมดของ Linked List เช่น การแทรก การลบ การค้นหา การอัพเดต การรวม Linked List สองรายการ การย้ายตำแหน่ง ฯลฯ

ที่นี่เราจะหารือเกี่ยวกับการดำเนินการของรายการที่เชื่อมโยงต่อไปนี้:

  • การใส่ที่หัว
  • การใส่ที่ส่วนท้าย
  • การแทรกหลังโหนด
  • การแทรกก่อนโหนด
  • ลบโหนดส่วนหัว
  • ลบโหนดส่วนท้าย
  • ค้นหาและลบโหนด
  • การสำรวจรายการที่เชื่อมโยง

นี่คือตัวอย่างของรายการเชื่อมโยงที่มีสี่โหนด

ตัวอย่างรายการลิงค์เดี่ยว
ตัวอย่างรายการลิงค์เดี่ยว

การแทรกที่ส่วนหัวของ Singly Linked List

เป็นการดำเนินการแบบง่ายๆ โดยทั่วไปเรียกว่าการผลักรายการลิงก์เดี่ยว คุณต้องสร้างโหนดใหม่และวางไว้ที่ส่วนหัวของรายการลิงก์

ในการดำเนินการนี้ เราต้องปฏิบัติตามเงื่อนไขสำคัญสองประการ ได้แก่

  1. หากรายการว่างเปล่า โหนดที่สร้างขึ้นใหม่จะเป็นโหนดหลัก และโหนดถัดไปของส่วนหัวจะเป็น ”NULL”
  2. หากรายการไม่ว่างเปล่า โหนดใหม่จะเป็นโหนดหลัก และโหนดถัดไปจะชี้ไปยังโหนดหลักก่อนหน้า

นี่คือรหัสหลอกสำหรับการแทรกโหนดที่ส่วนหัวของรายการที่เชื่อมโยง:

function insertAtHead( head, value ):
	newNode = Node(value)
	if head is NULL:
		head = newNode
		return head
	else: 
		newNode.next = head
		return newNode
การใส่ที่ศีรษะ
การใส่ที่ศีรษะ

การแทรกที่ส่วนท้ายของรายการเชื่อมโยงเดี่ยว

การแทรกโหนดที่ส่วนท้ายของรายการที่เชื่อมโยงมีความคล้ายคลึงกับการแทรกในส่วนหัว สิ่งที่คุณต้องทำคือ เคลื่อนที่ไปยังโหนดสิ้นสุดหรือโหนดส่วนท้าย จากนั้นชี้โหนด "ถัดไป" ของโหนดสิ้นสุดไปยังโหนดที่สร้างขึ้นใหม่ หากโหนดหลักเป็นโมฆะ โหนดใหม่จะเป็นโหนดหลัก

ต่อไปนี้เป็นขั้นตอน:

ขั้นตอน 1) เคลื่อนที่จนกระทั่งโหนด "ถัดไป" ของโหนดปัจจุบันกลายเป็นโมฆะ

ขั้นตอน 2) สร้างโหนดใหม่ด้วยค่าที่ระบุ

ขั้นตอน 3) กำหนดโหนดใหม่เป็นโหนดถัดไปของโหนดส่วนท้าย

รหัสเทียมสำหรับการแทรกโหนดที่ส่วนท้ายของรายการเดี่ยว:

function insertAtEnd( head, value ):
	newNode = Node(value)
	if head is NULL:
		head = newNode
		return head
	while head.next is not NULL:
		then head = head.next
	head.next = newNode
	newNode.next = NULL
การใส่ที่ส่วนท้าย
การสอดเข้าที่ส่วนหาง

การแทรกหลังโหนดในรายการที่เชื่อมโยงแบบเดี่ยว

การแทรกหลังโหนดมีสองส่วน: ค้นหาโหนดและแนบหลังโหนดที่ค้นหา เราจำเป็นต้องสำรวจโหนดทั้งหมด สำหรับแต่ละโหนด เราจำเป็นต้องจับคู่กับองค์ประกอบการค้นหา หากมีรายการที่ตรงกัน เราจะเพิ่มโหนดใหม่หลังโหนดที่ค้นหา

ต่อไปนี้เป็นขั้นตอน:

ขั้นตอน 1) สำรวจโหนดถัดไปจนกว่าค่าของโหนดปัจจุบันจะเท่ากับรายการค้นหา

ขั้นตอน 2) ตัวชี้ถัดไปของโหนดใหม่จะเป็นตัวชี้ถัดไปของโหนดปัจจุบัน

ขั้นตอน 3) โหนดถัดไปของโหนดปัจจุบันจะเป็นโหนดใหม่

นี่คือรหัสหลอกสำหรับการแทรกโหนดหลังโหนด:

function insertAfter( head, value, searchItem ):
	newNode = Node(value)
	while head.value equals searchItem:
		then head = head.next
	newNode.next = head.next.next
	head.next = newNode
การแทรกโหนดหลังโหนดในรายการที่เชื่อมโยงเดี่ยว
การแทรกโหนดหลังโหนดใน Singly Linked List

การแทรกก่อนโหนดในรายการที่เชื่อมโยงเดี่ยว

ฟังก์ชันนี้คล้ายกับการแทรกหลังโหนดมาก เราต้องสร้างโหนดใหม่และสำรวจรายการที่เชื่อมโยงจนกว่าโหนดปัจจุบันจะตรงกับโหนดการค้นหา หลังจากนั้นเราจะเพิ่มโหนดใหม่เป็นโหนดก่อนหน้าของโหนดปัจจุบัน

ต่อไปนี้เป็นขั้นตอน:

ขั้นตอน 1) เคลื่อนที่จนกว่าค่าของโหนดถัดไปจะเท่ากับรายการค้นหา

ขั้นตอน 2) สร้างโหนดใหม่และกำหนด "ถัดไป" ของโหนดโดยให้โหนดถัดจากโหนดถัดไปของโหนดปัจจุบัน

ขั้นตอน 3) กำหนดโหนดใหม่เป็นโหนดถัดไปของโหนดปัจจุบัน

นี่คือรหัสเทียม:

function insertBefore( head, value, searchItem ):
	newNode = Node(value)
	while head.next.value equals searchItem:
		then head = head.next
	newNode.next = head.next.next
	head.next = newNode
การแทรกโหนดก่อนโหนดในรายการที่เชื่อมโยงเดี่ยว
การแทรกโหนดก่อนโหนดในรายการที่เชื่อมโยงเดี่ยว

ลบส่วนหัวของรายการที่ลิงก์เดี่ยวๆ

ในทุกฟังก์ชันของรายการที่เชื่อมโยง จะมีการระบุเฮดพอยน์เตอร์เป็นพารามิเตอร์ คุณต้องลบเฮดโหนดและสร้างโหนดถัดไปของเฮดโหนดเป็นเฮดใหม่ของรายการที่เชื่อมโยง นอกจากนี้ เรายังต้องปลดปล่อยหน่วยความจำของโหนดที่ถูกลบ มิฉะนั้น หน่วยความจำจะถูกทำเครื่องหมายว่าใช้งานอยู่เมื่อโปรแกรมอื่นพยายามเข้าถึง

ต่อไปนี้เป็นขั้นตอนในการลบส่วนหัวของรายการที่เชื่อมโยงแบบเดี่ยว:

ขั้นตอน 1) กำหนดโหนดถัดไปของโหนดหลักเป็นส่วนหัวใหม่

ขั้นตอน 2) ปลดปล่อยหน่วยความจำที่จัดสรรโดยโหนดหลักก่อนหน้า

ขั้นตอน 3) คืนโหนดส่วนหัวใหม่

รหัสหลอกสำหรับการลบโหนดหลัก:

function deleteHead( head ):
	temp = head
	head = head.next
	free( temp )
	return head
การลบส่วนหัวของรายการที่เชื่อมโยง
การลบส่วนหัวของลิงค์ลิสต์

ลบส่วนท้ายของรายการที่เชื่อมโยงเดี่ยวๆ

การลบโหนดส่วนท้ายจะคุ้นเคยกับการลบโหนดส่วนหัวมากกว่า ข้อแตกต่างคือเราต้องข้ามไปยังจุดสิ้นสุดของรายการที่เชื่อมโยงแล้วลบออก ในรายการลิงค์เดี่ยว โหนดที่มีโหนดถัดไปเป็น “NULL” คือโหนดส่วนท้าย

ต่อไปนี้เป็นขั้นตอนในการลบโหนดท้ายของรายการที่เชื่อมโยง:

ขั้นตอน 1) ข้ามไปก่อนถึงส่วนท้าย บันทึกโหนดปัจจุบัน

ขั้นตอน 2) เพิ่มหน่วยความจำของโหนดถัดไปของโหนดปัจจุบัน

ขั้นตอน 3) ตั้งค่าโหนดถัดไปของโหนดปัจจุบันเป็น NULL

นี่คือรหัสหลอกสำหรับการลบโหนดท้าย:

function deleteTail( head ):
	while head.next.next is not NULL:
		head = head.next
	free( head.next )
	head.next = NULL
การลบส่วนท้ายของ Singly Linked List
การลบส่วนท้ายของ Singly Linked List

ค้นหาและลบโหนดออกจากรายการลิงก์เดี่ยว

ฟังก์ชันนี้มีสองงาน คือ ค้นหาและลบ เราจำเป็นต้องค้นหาจนถึงจุดสิ้นสุดของรายการที่เชื่อมโยงเดี่ยวๆ หากเราพบโหนดที่คล้ายกัน เราจะลบโหนดนั้นออก หลังจากนั้นเราจำเป็นต้องผสานหรือเชื่อมโยงโหนดด้านซ้ายและขวาของโหนดที่ถูกลบ ต่อไปนี้เป็นขั้นตอนในการดำเนินการนี้:

ขั้นตอน 1) ข้ามไปจนสุดรายการลิงก์ ตรวจสอบว่าโหนดปัจจุบันเท่ากับโหนดการค้นหาหรือไม่

ขั้นตอน 2) หากโหนดใดตรงกัน ให้จัดเก็บตัวชี้โหนดไปยังโหนดปัจจุบัน

ขั้นตอน 3) “ถัดไป” ของโหนดก่อนหน้าจะเป็นโหนดถัดไปของโหนดปัจจุบัน

ขั้นตอน 4) ลบและเพิ่มหน่วยความจำของโหนดปัจจุบัน

รหัสเทียมสำหรับการค้นหาและลบโหนดออกจากรายการที่เชื่อมโยงโดยลำพัง:

function searchAndDelete( head, searchItem ):
	while head.next.next is not NULL  and head.next.value is not equals searchItem :
		head = head.next
	head.next = head.next.next
	delete(head.next)
ค้นหาและลบโหนดจากรายการเชื่อมโยงเดี่ยว
ค้นหาและลบโหนดออกจาก Singly Linked List

สำรวจรายการที่เชื่อมโยงเพียงรายการเดียว

รายการที่เชื่อมโยงเดี่ยวมีฟังก์ชันสำหรับการสำรวจตั้งแต่หัวจรดท้ายเท่านั้น ไม่มีตัวชี้ไปยังโหนดก่อนหน้า นั่นเป็นสาเหตุที่เราไม่สามารถสำรวจรายการเชื่อมโยงเดี่ยวในลำดับย้อนกลับได้ เราจะสำรวจแต่ละโหนดและพิมพ์ค่าของโหนดปัจจุบันจนกว่าเราจะได้ค่าว่าง

ต่อไปนี้เป็นขั้นตอน:

ขั้นตอน 1) สำรวจแต่ละโหนดจนกว่าเราจะได้ค่าว่างเป็นโหนดถัดไป

ขั้นตอน 2) พิมพ์ค่าของโหนดปัจจุบัน

รหัสหลอกสำหรับการสำรวจรายการที่เชื่อมโยงโดยลำพัง:

function traverse( head ):
		while head.next is not NULL:
			print the value of the head
			head = head.next

ตัวอย่างรายการลิงค์เดี่ยวใน C++

#include<iostream>
using namespace std;
struct Node{
  int data;
  struct Node *next;
};
void insertAtHead(Node* &head, int value){
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  if(head!=NULL){
    newNode->next = head;
  }
  head = newNode;
  cout<<"Added "<<newNode->data<<" at the front"<<endl;
}
void insertEnd(Node* &head, int value){
  if(head==NULL){
    insertAtHead(head,value);
  }
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  Node *temp = head;
  while(temp->next!=NULL){
    temp = temp->next;
  }
  temp->next = newNode;
  cout<<"Added "<<newNode->data<<" at the end"<<endl;
}
void searchAndDelete(Node **headPtr, int searchItem){
  Node *temp = new Node();
  if( (*headPtr)->data == searchItem ){
    temp = *headPtr;
    *headPtr = (*headPtr)->next;
    free(temp);
  }else{
    Node *currentNode = *headPtr;
    while(currentNode->next != NULL){
      if(currentNode->next->data == searchItem){
        temp = currentNode->next;
        currentNode->next = currentNode->next->next;
        free(temp);
      }else{
        currentNode = currentNode->next;
      }
    }
  }
  cout<<"Deleted Node\t"<<searchItem<<endl;
  return;
}
void insertAfter(Node * &headPtr, int searchItem, int value){
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  Node *head = headPtr;
  while(head->next!=NULL &&  head->data!=searchItem){
    head = head->next;
  }
  newNode->next = head->next;
  head->next = newNode;
  cout<<"Inserted "<<value<<" after node\t"<<searchItem<<endl;
}
void insertBefore(Node * &headPtr, int searchItem, int value){
  Node* newNode = new Node();
  newNode->data = value;
  newNode->next = NULL;
  Node *head = headPtr;
  while(head->next!=NULL &&  head->next->data!=searchItem){
    head = head->next;
  }
  newNode->next = head->next;
  head->next = newNode;
  cout<<"Inserted "<<value<<" before node\t"<<searchItem<<endl;
}
void traverse(Node *headPointer){  
  Node* tempNode = new Node();
  tempNode = headPointer;
  cout<<"Traversal from head:\t";
  while(tempNode !=NULL){
    cout<<tempNode->data;
    if(tempNode->next)
      cout<<" --> ";
    tempNode = tempNode ->next;
  }
  cout<<endl;
}
int main(){
  Node *head = NULL;
  insertAtHead(head,5);
  insertAtHead(head,6);
  insertAtHead(head,7);
  insertEnd(head,9);
  traverse(head);
  searchAndDelete(&head,6);
  traverse(head);
  insertAfter(head,7,10);
  insertBefore(head,9,11);
  traverse(head);
}

Output:

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

ตัวอย่างรายการลิงค์เดี่ยวใน Python โครงการ

class Node:
  def __init__(self,data = None, next = None):
    self.data = data
    self.next = next
class SinglyLinkedList:
  def __init__(self):
    self.head = None 
  def insertAtHead(self, value):
    newNode = Node(data=value)    
    if self.head is not None:
      newNode.next = self.head
    self.head = newNode
    print(f'Added {newNode.data} at the front.')
    return  
  def insertAtEnd(self,value):
    if self.head is None:
      self.insertAtHead(value)
    newNode = Node(value)
    temp = self.head
    while temp.next is not None:
      temp = temp.next
    temp.next = newNode
    print(f'Added {newNode.data} at the end.')  
  def searchAndDelete(self,searchItem):
    temp = Node()
    if self.head is searchItem:
      temp = self.head
      self.head = self.head.next
    else:
      currentNode = self.head
      while currentNode.next is not None:
        if currentNode.next.data is searchItem:
          temp = currentNode.next
          currentNode.next = currentNode.next.next
        else:
          currentNode = currentNode.next
    print(f'Deleted node\t{searchItem}')
    return
  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
    print(f'Inserted {value} after node\t{searchItem}')
    return 
  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
    print(f'Inserted {value} before node\t{searchItem}')
    return
  def traverse(self):
    temp = self.head
    print("Traversing from head:\t",end="")
    while temp:
      print("{}\t".format(temp.data),end="")
      temp = temp.next
    print()
SinglyLinkedList = SinglyLinkedList()
SinglyLinkedList.insertAtHead(5)
SinglyLinkedList.insertAtHead(6)
SinglyLinkedList.insertAtHead(7)
SinglyLinkedList.insertAtEnd(9)
SinglyLinkedList.traverse()
SinglyLinkedList.searchAndDelete(6)
SinglyLinkedList.traverse()
SinglyLinkedList.insertAfter(7,10)
SinglyLinkedList.insertbefore(9, 11)
SinglyLinkedList.traverse()

Output:

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

ความซับซ้อนของรายการที่เชื่อมโยงแบบเดี่ยว

ความซับซ้อนมีอยู่ 2 ประเภท ได้แก่ ความซับซ้อนของเวลาและความซับซ้อนของพื้นที่ ความซับซ้อนของเวลาที่แย่ที่สุดและโดยเฉลี่ยจะเหมือนกันสำหรับรายการลิงก์เดี่ยว

ความซับซ้อนของเวลาสำหรับกรณีที่ดีที่สุด:

  • การใส่ส่วนหัวสามารถทำได้ใน O(1) เนื่องจากเราไม่จำเป็นต้องสำรวจภายในรายการที่เชื่อมโยง
  • การค้นหาและการลบสามารถทำได้ใน O(1) หากมีองค์ประกอบการค้นหาอยู่ในโหนดหัว

ความซับซ้อนของเวลาสำหรับกรณีเฉลี่ย:

  • การแทรกภายในรายการเชื่อมโยงจะใช้ O(n) หากมีองค์ประกอบ “n” อยู่ในรายการเชื่อมโยงเดี่ยว
  • การค้นหาและการลบอาจใช้ O(n) เช่นกัน เนื่องจากองค์ประกอบการค้นหาสามารถแสดงอยู่ในโหนดส่วนท้ายได้ ในกรณีนั้น คุณควรสำรวจรายการทั้งหมด

ความซับซ้อนของพื้นที่ของรายการลิงก์เดี่ยว

Singly Linked List จะจัดสรรหน่วยความจำแบบไดนามิก หากเราต้องการจัดเก็บ n องค์ประกอบ ระบบจะจัดสรรหน่วยความจำ n หน่วย ดังนั้นความซับซ้อนของพื้นที่คือ O(n)