بررسی لیست پیوندی
بر خلاف list که عناصر آرایه وار مستقیم روی حافظه ذخیره میشن، در لیست پیوندی (LinkedList) عناصر در گره (node) ها ذخیره میشن.
در لیست پیوندی (LinkedList) هر عنصر داخل یک node ذخیره میشه و هر node به node بعدی اشاره میکنه و آخرین node در حالت استاندارد به null اشاره میکنه؛ به عبارتی هر node به node بعدی زنجیر (link) میشه.
زمانی که LinkedList خالی باشه تنها یک node داخلش وجود داره، مقدار عنصری که داخل node نگهداری میشه null و node بعدی که بهش اشاره میکنه null است.
زمانی که بخوایم عناصر رو از ابتدا یا انتها حذف یا اضافه کنیم، استفاده از LinkedList بهینه تر از استفاده از list است؛ اما اگه بخوایم به عناصر با index به صورت تصادفی دسترسی داشته باشیم استفاده از list بهینه تر از LinkedList است.
ساختار لیست پیوندی
هر عنصر داخل یک node نگهداری میشه و هر node به node بعد از خودش node (در صورتی که اخرین node نباشه) و به node قبل از خودش (در صورتی که اولین node نباشه) اشاره میکنه (Doubly LinkedList).
در زیر به صورت تصویری شرح داده شده که چطور عناصر در LinkedList نگهداری میشن.
انواع لیست پیوندی
انواع مختلفی از پیاده سازی لیست پیوندی (LinkedList) داریم که در ادامه سه تا از پیاده سازی های معروف رو بیان میکنیم.
توجه
در این مطلب فقط به جزییات Doubly LinkedList می پردازیم، نحوه ی پیاده سازی سایر LinkedList ها تقریبا مشابه است.
لیست پیوندی یک طرفه (Singly LinkedList)
در این نوع لیست پیوندی (LinkedList) هر node به node بعد خودش اشاره (point) میکنه تا آخرین node که node بعدش null است.
جستجو در این نوع LinkedList یک طرفه است یعنی برای پیدا کردن یک عنصر، جستجو از اولین node شروع میشه تا به node مورد نظر برسیم.
لیست پیوندی دو طرفه (Doubly LinkedList)
در این نوع لیست پیوندی هر node به node بعد از خودش و node قبل از خودش اشاره (point) میکنه و اولین node به null به عنوان node قبل از خودش اشاره میکنه و اخرین node به null به عنوان node بعد از خودش اشاره میکنه.
جستجو در این نوع LinkedList دو طرفه است، یعنی جستجو برای پیدا کردن یک عنصر، هم از اولین node به آخرین node امکان پذیر است هم از اخرین node به اولین node
لیست پیوندی مُدور (Circular LinkedList)
در این نوع LinkedList اخرین node بجای اشاره به null به عنوان node بعد خودش به اولین node در LinkedList اشاره می کنه.
بررسی گره (node) در لیست پیوندی
با افزودن هر عنصر یک گره (node) برای عنصر ایجاد میشه و عنصر داخل node نگهداری میشه؛ node علاوه بر نگهداری عنصر، به node قبل یا بعد خودش اشاره میکنه و اینجوری یک زنجیره بین عناصر توسط node ها ایجاد میشه.
به زبون ساده هر عنصر به عنصر بعد یا قبل از خودش به وسیله ی node گره زده میشه و این گره ها لیست پیوندی (LinkedList) رو تشکیل میدن.
فرم کلی node:
record node{
Node next
Node prev
E element
}
افزودن عنصر به لیست پیوندی
با افزودن هر عنصر به LinkedList یک node برای نگهداری عنصر ایجاد میشه، در LinkedList میتونیم node ها رو به ابتدا، انتها یا وسط سایر node ها اضافه کنیم.
افزودن node به ابتدای LinkedList
اگه بخوایم node رو به ابتدای LinkedList اضافه کنیم، node ایجاد شده به اولین node موجود در LinkedList اشاره میکنه. پیچیدگی زمان (time complexity) در این حالت O(1) است.
function addFirst(e: E){
newNode := Node(e)
if size = 0
lastNode := newNode
firstNode := newNode
else
newNode.next := firstNode
firstNode.prev := newNode
firstNode := newNode
size++
}
افزودن node به انتهای لیست پیوندی
اگه بخوایم node رو به انتهای LinkedList اضافه کنیم، اخرین node در LinkedList به node جدید اشاره میکنه؛ پیچیدگی زمان در این حالت O(1) است.
function addLast(e: E){
newNode := Node(e)
if size = 0
lastNode := newNode
firstNode := newNode
else
lastNode.next := newNode
newNode.prev := lastNode
lastNode := newNode
size++
}
افزودن node به وسط لیست پیوندی
اگه بخوایم node رو وسط node ها اضافه کنیم، node قبلی به node جدید اشاره میکنه و node جدید به node بعد؛ پیچیدگی زمان در این حالت O(n) است.
function add(index: int, e: E){
if index < 0 or index > size - 1
throw IndexOutOfBoundException
newNode := Node(e)
if firstNode = null
firstNode := newNode
lastNode := newNode
size++
else
count := 0
currentNode := firstNode
while count != index
currentNode := currentNode.next
count++
newNode.next = currentNode.next
currentNode.next = newNode
size++
}
صدا زدن عنصر با index در لیست پیوندی
برای صدا زدن یک عنصر با index در LinkedList باید node مربوط به index مورد نظر رو پیدا کنیم؛ صدا زدن یک عنصر در LinkedList دارای پیچیدگی زمان O(n) است.
function get(index: int): E {
if index < 0 or index > size - 1
throw IndexOutOfBoundException
count := 0
node := firstNode
while count != index
node := node.next
return node.element
}
حذف عنصر از لیست پیوندی
برای حذف یک عنصر از LinkedList باید node که عنصر مورد نظر رو نگهداری میکنه از زنجیره خارج کنیم؛ میتونیم یک عنصر رو از ابتدا، انتها یا وسط یک LinkedList حذف کنیم.
حذف عنصر از ابتدای LinkedList
برای حذف اولین عنصر از LinkedList، باید اولین node در LinkedList رو از زنجیره خارج کنیم.
function removeFirst(): E {
Node removedNode := null
if size = 0
throw NoSuchElementException
if size = 1
removedNode := firstNode
firstNode := null
lastNode := null
else
removedNode := firstNode
firstNode.next := firstNode.next.next
firstNode := firstNode.next
return removedNode.element
}
حذف عنصر از انتهای لیست پیوندی
برای حذف کردن آخرین عنصر از LinkedList، آخرین node رو از زنجیره خارج کنیم.
توجه
حذف اخرین node در Singly LinkedList دارای پیچیدگی زمان O(n) و در Doubly LinkedList دارای پیچیدگی زمان O(1) است؛ اما همانطور که گفتیم در این مطلب به Doubly LinkedList می پردازیم.
function removeLast(): E{
Node removedNode := null
if size = 0
throw NoSuchElementException
if size = 1
removedNode := firstNode
firstNode := null
lastNode := null
else
prevNode = lastNode.prev
prevNode.next = null
lastNode := prevNode
return removedNode.element
}
حذف کردن عنصر با استفاده از index در لیست پیوندی
با استفاده از index در LinkedList میتونیم یک عنصر رو در ابتدا، وسط یا انتهای LinkedList حذف کنیم، برای این کار کافیه node مربوط به index رو پیدا کرده و از زنجیره حذفش کنیم.
function remove(index: int): E {
Node removedNode := null
if size = 0
throw NoSuchElementException
if index < 0 or index > size - 1
throw IndexOutOfBoundException
if size = 1
removedNode := firstNode
firstNode := null
lastNode := null
else
count := 0
Node node := firstNode
while count != index
count++
node := node.next
prevNode := node.prev
prevNode.next := node.next
return node.element
}
پیاده سازی لیست پیوندی در زبان های برنامه نویسی
میخوایم لیست پیوندی (LinkedList) رو به صورت عملی با استفاده از زبان های برنامه نویسی (جاوا و کاتلین) پیاده سازی کنیم.
در این پیاده سازی توابع remove(index: int), add(index: int, element: E), removeLast(): E به عنوان تمرین رها شده تا خودتون انجامش بدید.
public class MyLinkedList<E> {
private Node<E> firstNode;
private Node<E> lastNode;
private int size;
public boolean isEmpty(){
return size == 0;
}
public int getSize() {
return size;
}
public void addFirst(E element){
Node<E> newNode = new Node<>(element, null, null);
if (isEmpty()){
firstNode = newNode;
lastNode = newNode;
}else{
firstNode.prev = newNode;
newNode.next = firstNode;
firstNode = newNode;
}
size++;
}
public void addLast(E element){
Node<E> newNode = new Node<>(element, null, null);
if (isEmpty()){
firstNode = newNode;
lastNode = newNode;
}else {
lastNode.next = newNode;
newNode.prev = lastNode;
lastNode = newNode;
}
size++;
}
public E getFirst(){
if(isEmpty()) throw NoSuchElementException("The LinkedList is empty");
return firstNode.element;
}
public E getLast(){
if(isEmpty()) throw NoSuchElementException("The LinkedList is empty");
return lastNode.element;
}
public E get(int index){
if (index < 0 || index > size - 1)
throw new IndexOutOfBoundsException("The index should be between 0 and " + (size -1));
Node<E> node;
if (size == 1){
node = firstNode;
}else {
int count = 0;
Node<E> currentNode = firstNode;
while (count != index){
currentNode = currentNode.next;
count++;
}
node = currentNode;
}
return node.element;
}
public E removeFirst(){
if (isEmpty())
throw new NoSuchElementException("The list is empty");
Node<E> removedNode;
if (size == 1){
removedNode = firstNode;
firstNode = null;
lastNode = null;
}else {
removedNode = firstNode;
firstNode = firstNode.next;
firstNode.prev = null;
}
size--;
return removedNode.element;
}
public E add(int index, E element){
//TODO Left as an exercise
return null;
}
public E remove(int index){
//TODO Left as an exercise
return null;
}
public E removeLast(){
//TODO Left as an exercise
return null;
}
private static final class Node<E> {
private final E element;
private Node<E> next;
private Node<E> prev;
Node(E element, Node<E> next, Node<E> prev) {
this.element = element;
this.next = next;
this.prev = prev;
}
}
}
class MyLinkedList<E> {
private var firstNode: Node<E>? = null
private var lastNode: Node<E>? = null
var size = 0
val empty get() = size == 0
fun addFirst(element: E){
val newNode: Node<E> = Node(element)
if (size == 0) {
firstNode = newNode
lastNode = newNode
}else{
newNode.next = firstNode
firstNode!!.prev = newNode
firstNode = newNode
}
size++
}
fun addLast(element: E){
if (size == 0){
firstNode = Node(element)
lastNode = Node(element)
}else{
val node = Node(element)
node.prev = lastNode
lastNode!!.next = node
lastNode = node
}
size++
}
fun getFirst(): E?{
return firstNode?.element
}
fun getLast(): E?{
return lastNode?.element
}
fun get(index: Int): E? {
if (index !in 0..<size)
throw IndexOutOfBoundsException("index should be between 0 to ${size - 1}")
if (empty)
throw NoSuchElementException("The List is empty")
var node: Node<E>?
var count = 0
var currentNode = firstNode
while (count != index ){
currentNode = currentNode?.next
count++
}
node = currentNode
return node?.element
}
fun removeFirst(): E {
if (empty)
throw NoSuchElementException("There's no element in list")
val node: Node<E>
if (size == 1) {
node = firstNode!!
firstNode = null
lastNode = null
} else{
node = firstNode!!
firstNode = firstNode!!.next
firstNode!!.prev = null
}
size--
return node.element
}
fun add(index: Int, element: E){
//TODO left as an exercise
}
fun removeLast(): E? {
//TODO left as an exercise
return null
}
fun remove(index: Int): E?{
//TODO left as an exercise
return null
}
private data class Node<E>(val element: E, var next: Node<E>? = null, var prev: Node<E>? = null)
}
استفاده از لیست پیوندی در زبان های برنامه نویسی
در بسیاری از زبان های برنامه نویسی لیست پیوندی (LinkedList) به عنوان یک کتابخونه ی استاندارد پیاده سازی شده و در اینجا میخوایم استفاده از LinkedList رو بررسی کنیم.
LinkedList<String> colors = new LinkedList<>();
colors.add("Blue");
colors.add("Pink");
colors.add("Purple");
colors.addFirst("Cyan");
colors.add(1, "Red");
colors.removeLast();
colors.addLast("Yellow");
System.out.println("Display colors ");
for(int i =0; i<colors.size(); i++){
String c = colors.get(i);
System.out.print(c + " ");
}
System.out.println("Remove and Display colors backward: ");
while(!colors.isEmpty()){
String c = colors.removeLast();
System.out.println(c);
}
val colors: LinkedList<String> = LinkedList<>();
colors.add("Blue")
colors.add("Pink")
colors.add("Purple")
colors.addFirst("Cyan")
colors.add(1, "Red")
colors.removeLast()
colors.addLast("Yellow")
System.out.println("Display colors ");
for(i in 0 until colors.size ){
val c = colors.get(i)
print("$c ")
}
println("Remove and Display colors backward: ")
while(!colors.isEmpty()){
val c = colors.removeLast()
println(c)
}
خلاصه
- در LinkedList عناصر داخل node ها ذخیره میشن.
- هر node در LinkedList به node بعدی اشاره میکنه.
- انواع پیاده سازی LinkedList وجود داره از جمله: Singly LinkedList, Doubly LinkedList و Circular LinkedList
- در Singly LinkedList، یک node به node بعدی اشاره میکنه
- در Doubly LinkedList یک node به node بعد و node قبل از خودش اشاره میکنه (Doubly LinkedList).
- پیچیدگی زمان هنگام افزودن یک عنصر به ابتدا یا انتهای یک LinkedList O(1) و هنگام افزودن یک عنصر با index برابر با O(n) است.
- پیچیدگی زمان برای صدا زدن یک عنصر در LinkedList با index برابر با O(n) است.
- پیچیدگی زمان (time complexity) برای حذف کردن یک عنصر از ابتدا یا انتهای یک LinkedList برابر با O(1) و برای حذف کردن یک عنصر وسط LinkedList برابر با O(n) است.