sinau-c/content/linked_lists.md

418 lines
13 KiB
Markdown

---LESSON_INFO---
**Learning Objectives:**
- Memahami konsep daftar tertaut (linked list) dalam bahasa C
- Belajar membuat dan mengelola struktur daftar tertaut
- Mengenal operasi dasar pada daftar tertaut
- Memahami perbedaan antara array dan daftar tertaut
**Prerequisites:**
- Pemahaman tentang pointer dan struktur
- Pemahaman dasar tentang alokasi memori dinamis
---END_LESSON_INFO---
# Daftar Tertaut dalam C
## Pengantar
Daftar tertaut adalah contoh terbaik dan paling sederhana dari struktur data dinamis yang menggunakan pointer untuk implementasinya. Namun, memahami pointer sangat penting untuk memahami bagaimana daftar tertaut bekerja, jadi jika Anda melewatkan tutorial pointer, Anda harus kembali dan mengulanginya. Anda juga harus familiar dengan alokasi memori dinamis dan struktur.
Pada dasarnya, daftar tertaut berfungsi sebagai array yang bisa berkembang dan menyusut sesuai kebutuhan, dari titik mana pun dalam array.
Daftar tertaut memiliki beberapa keunggulan dibandingkan array:
1. Item bisa ditambahkan atau dihapus dari tengah daftar
2. Tidak perlu mendefinisikan ukuran awal
Namun, daftar tertaut juga memiliki beberapa kelemahan:
1. Tidak ada akses "acak" - tidak mungkin mencapai item ke-n dalam array tanpa terlebih dahulu mengiterasi semua item hingga item yang diinginkan. Ini berarti kita harus mulai dari awal daftar dan menghitung berapa kali kita maju dalam daftar sampai kita sampai ke item yang diinginkan.
2. Membutuhkan alokasi memori dinamis dan pointer, yang memperumit kode dan meningkatkan risiko kebocoran memori dan kesalahan segmen.
3. Daftar tertaut memiliki overhead yang jauh lebih besar dibandingkan array, karena item daftar tertaut dialokasikan secara dinamis (yang kurang efisien dalam penggunaan memori) dan setiap item dalam daftar juga harus menyimpan pointer tambahan.
## Apa itu daftar tertaut?
Daftar tertaut adalah kumpulan node yang dialokasikan secara dinamis, disusun sedemikian rupa sehingga setiap node berisi satu nilai dan satu pointer. Pointer selalu menunjuk ke anggota berikutnya dari daftar. Jika pointer adalah NULL, maka itu adalah node terakhir dalam daftar.
Daftar tertaut dipegang menggunakan variabel pointer lokal yang menunjuk ke item pertama dari daftar. Jika pointer itu juga NULL, maka daftar dianggap kosong.
```
------------------------------
| | |
| DATA | NEXT |------>
| | |
------------------------------
```
Mari kita definisikan node daftar tertaut:
```c
typedef struct node {
int val;
struct node * next;
} node_t;
```
Perhatikan bahwa kita mendefinisikan struct secara rekursif, yang mungkin dalam C. Mari kita beri nama tipe node kita `node_t`.
Sekarang kita bisa menggunakan node. Mari kita buat variabel lokal yang menunjuk ke item pertama dari daftar (disebut `head`).
```c
node_t * head = NULL;
head = (node_t *) malloc(sizeof(node_t));
if (head == NULL) {
return 1;
}
head->val = 1;
head->next = NULL;
```
Kita baru saja membuat variabel pertama dalam daftar. Kita harus mengatur nilai, dan item berikutnya menjadi kosong, jika kita ingin selesai mengisi daftar. Perhatikan bahwa kita harus selalu memeriksa apakah malloc mengembalikan nilai NULL atau tidak.
Untuk menambahkan variabel ke akhir daftar, kita bisa terus maju ke pointer berikutnya:
```c
node_t * head = NULL;
head = (node_t *) malloc(sizeof(node_t));
head->val = 1;
head->next = (node_t *) malloc(sizeof(node_t));
head->next->val = 2;
head->next->next = NULL;
```
Ini bisa terus berjalan, tetapi yang seharusnya kita lakukan adalah maju ke item terakhir dari daftar, sampai variabel `next` akan menjadi `NULL`.
## Iterasi melalui daftar
Mari kita buat fungsi yang mencetak semua item dari daftar. Untuk melakukan ini, kita perlu menggunakan pointer `current` yang akan terus melacak node yang sedang kita cetak. Setelah mencetak nilai node, kita mengatur pointer `current` ke node berikutnya, dan mencetak lagi, sampai kita mencapai akhir dari daftar (node berikutnya adalah NULL).
```c
void print_list(node_t * head) {
node_t * current = head;
while (current != NULL) {
printf("%d\\n", current->val);
current = current->next;
}
}
```
## Menambahkan item ke akhir daftar
Untuk mengiterasi semua anggota dari daftar tertaut, kita gunakan pointer yang disebut `current`. Kita atur untuk mulai dari head dan kemudian di setiap langkah, kita majukan pointer ke item berikutnya dalam daftar, sampai kita mencapai item terakhir.
```c
void push(node_t * head, int val) {
node_t * current = head;
while (current->next != NULL) {
current = current->next;
}
/* sekarang kita bisa menambahkan variabel baru */
current->next = (node_t *) malloc(sizeof(node_t));
current->next->val = val;
current->next->next = NULL;
}
```
Kasus penggunaan terbaik untuk daftar tertaut adalah tumpukan dan antrian, yang akan kita implementasikan sekarang:
## Menambahkan item ke awal daftar (pushing ke daftar)
Untuk menambahkan ke awal daftar, kita perlu melakukan hal berikut:
1. Buat item baru dan atur nilainya
2. Hubungkan item baru ke pointer ke head dari daftar
3. Atur head dari daftar menjadi item baru kita
Ini akan secara efektif membuat head baru untuk daftar dengan nilai baru, dan menjaga sisa daftar terhubung ke sana.
Karena kita menggunakan fungsi untuk melakukan operasi ini, kita ingin bisa memodifikasi variabel head. Untuk melakukan ini, kita harus melewati pointer ke variabel pointer (pointer ganda) sehingga kita bisa memodifikasi pointer itu sendiri.
```c
void push(node_t ** head, int val) {
node_t * new_node;
new_node = (node_t *) malloc(sizeof(node_t));
new_node->val = val;
new_node->next = *head;
*head = new_node;
}
```
## Menghapus item pertama (popping dari daftar)
Untuk pop variabel, kita perlu membalikkan tindakan ini:
1. Ambil item berikutnya yang ditunjuk oleh head dan simpan
2. Bebaskan item head
3. Atur head menjadi item berikutnya yang telah kita simpan di sisi
Berikut adalah kode:
```c
int pop(node_t ** head) {
int retval = -1;
node_t * next_node = NULL;
if (*head == NULL) {
return -1;
}
next_node = (*head)->next;
retval = (*head)->val;
free(*head);
*head = next_node;
return retval;
}
```
## Menghapus item terakhir dari daftar
Menghapus item terakhir dari daftar sangat mirip dengan menambahkannya ke akhir daftar, tetapi dengan satu perbedaan besar - karena kita harus mengubah satu item sebelum item terakhir, kita sebenarnya harus melihat dua item ke depan dan melihat apakah item berikutnya adalah item terakhir dalam daftar:
```c
int remove_last(node_t * head) {
int retval = 0;
/* jika hanya ada satu item dalam daftar, hapus */
if (head->next == NULL) {
retval = head->val;
free(head);
return retval;
}
/* dapatkan ke item kedua dari akhir dalam daftar */
node_t * current = head;
while (current->next->next != NULL) {
current = current->next;
}
/* sekarang current menunjuk ke item kedua dari akhir item dari daftar, jadi mari kita hapus current->next */
retval = current->next->val;
free(current->next);
current->next = NULL;
return retval;
}
```
## Menghapus item tertentu
Untuk menghapus item tertentu dari daftar, baik berdasarkan indeksnya dari awal daftar atau berdasarkan nilainya, kita perlu melewati semua item, terus melihat ke depan untuk mengetahui apakah kita telah mencapai node sebelum item yang ingin kita hapus. Ini karena kita perlu mengubah lokasi ke mana pointer node sebelumnya menunjuk juga.
Berikut adalah algoritma:
1. Iterasi ke node sebelum node yang ingin kita hapus
2. Simpan node yang ingin kita hapus dalam pointer sementara
3. Atur pointer next dari node sebelumnya untuk menunjuk ke node setelah node yang ingin kita hapus
4. Hapus node menggunakan pointer sementara
Ada beberapa kasus khusus yang perlu kita tangani, jadi pastikan Anda memahami kode.
```c
int remove_by_index(node_t ** head, int n) {
int i = 0;
int retval = -1;
node_t * current = *head;
node_t * temp_node = NULL;
if (n == 0) {
return pop(head);
}
for (i = 0; i < n-1; i++) {
if (current->next == NULL) {
return -1;
}
current = current->next;
}
if (current->next == NULL) {
return -1;
}
temp_node = current->next;
retval = temp_node->val;
current->next = temp_node->next;
free(temp_node);
return retval;
}
```
---
## Tabel Operasi pada Daftar Tertaut
| Operasi | Deskripsi | Contoh |
|---------|-----------|--------|
| Push | Menambahkan item ke awal daftar | `push(&head, value)` |
| Pop | Menghapus item dari awal daftar | `pop(&head)` |
| Print | Mencetak semua elemen daftar | `print_list(head)` |
| Remove | Menghapus item berdasarkan nilai | `remove_by_value(&head, value)` |
---EXERCISE---
# Latihan: Menghapus Elemen Berdasarkan Nilai
Anda harus mengimplementasikan fungsi `remove_by_value` yang menerima pointer ganda ke head dan menghapus item pertama dalam daftar yang memiliki nilai `val`.
**Requirements:**
- Gunakan pointer ganda untuk mengakses head
- Hapus node pertama yang memiliki nilai tertentu
- Bebaskan memori dari node yang dihapus
- Tangani kasus ketika head perlu dihapus
**Expected Output:**
```
1
2
4
```
Try writing your solution in the code editor below!
---KEY_TEXT---
previous->next = current->next
free(current)
current = current->next
pop(head)
---END_KEY_TEXT---
---EXPECTED_OUTPUT---
1
2
4
---END_EXPECTED_OUTPUT---
---INITIAL_CODE---
#include <stdio.h>
#include <stdlib.h>
typedef struct node {
int val;
struct node * next;
} node_t;
void print_list(node_t * head) {
node_t * current = head;
while (current != NULL) {
printf("%d\\n", current->val);
current = current->next;
}
}
int pop(node_t ** head) {
int retval = -1;
node_t * next_node = NULL;
if (*head == NULL) {
return -1;
}
next_node = (*head)->next;
retval = (*head)->val;
free(*head);
*head = next_node;
return retval;
}
int remove_by_value(node_t ** head, int val) {
/* TODO: isi kode Anda di sini */
}
int main() {
node_t * test_list = (node_t *) malloc(sizeof(node_t));
test_list->val = 1;
test_list->next = (node_t *) malloc(sizeof(node_t));
test_list->next->val = 2;
test_list->next->next = (node_t *) malloc(sizeof(node_t));
test_list->next->next->val = 3;
test_list->next->next->next = (node_t *) malloc(sizeof(node_t));
test_list->next->next->next->val = 4;
test_list->next->next->next->next = NULL;
remove_by_value(&test_list, 3);
print_list(test_list);
}
---END_INITIAL_CODE---
---SOLUTION_CODE---
#include <stdio.h>
#include <stdlib.h>
typedef struct node {
int val;
struct node * next;
} node_t;
void print_list(node_t * head) {
node_t * current = head;
while (current != NULL) {
printf("%d\\n", current->val);
current = current->next;
}
}
int pop(node_t ** head) {
int retval = -1;
node_t * next_node = NULL;
if (*head == NULL) {
return -1;
}
next_node = (*head)->next;
retval = (*head)->val;
free(*head);
*head = next_node;
return retval;
}
int remove_by_value(node_t ** head, int val) {
node_t *previous, *current;
if (*head == NULL) {
return -1;
}
if ((*head)->val == val) {
return pop(head);
}
previous = *head;
current = (*head)->next;
while (current) {
if (current->val == val) {
previous->next = current->next;
free(current);
return val;
}
previous = current;
current = current->next;
}
return -1;
}
void delete_list(node_t *head) {
node_t *current = head, *next = head;
while (current) {
next = current->next;
free(current);
current = next;
}
}
int main(void) {
node_t * test_list = (node_t *) malloc(sizeof(node_t));
test_list->val = 1;
test_list->next = (node_t *) malloc(sizeof(node_t));
test_list->next->val = 2;
test_list->next->next = (node_t *) malloc(sizeof(node_t));
test_list->next->next->val = 3;
test_list->next->next->next = (node_t *) malloc(sizeof(node_t));
test_list->next->next->next->val = 4;
test_list->next->next->next->next = NULL;
remove_by_value(&test_list, 3);
print_list(test_list);
delete_list(test_list);
return EXIT_SUCCESS;
}
---END_SOLUTION_CODE---