Hỏi về đệ quy trong danh sách liên kết đơn

Em chào các anh chị,
Em có 1 bài tập là cho ntn.

struct LNode{
    int ID; // giá trị khóa của con trỏ
    struct LNode* tieptheo; // con trỏ trỏ đến nút kế tiếp
}

Đề bài: Lập trình hàm struct LNode* loaibo(struct LNode* h) thực hiện loại bỏ nút cuối cùng của ds liên kết đơn có con trỏ đầu là h và trả về con trỏ đến đầu danh sách mới thu được bằng phương pháp đệ quy.

Đối với việc xóa bằng while thì em ok, nhưng xóa bằng đệ quy và trả về con trỏ đầu tiên thì em chưa làm được( Do không có ý tưởng)
Em gửi mã nguồn. Em viết trên Dev C++, anh em có thể sử dụng https://www.codechef.com/ide để code nhé.
Cám ơn mọi người giúp đỡ

#include <stdio.h>
#include <stdlib.h>

struct LNode{
	int ID;
	struct LNode* tieptheo;
};

struct LNode* init(){
	struct LNode* head = (struct LNode*)malloc(sizeof(struct LNode*));
	head = NULL;
	return head;
}
struct LNode* createNode(int v){
	struct LNode* newNode = (struct LNode*)malloc(sizeof(struct LNode*));
	newNode->ID = v;
	newNode->tieptheo = NULL;
	return newNode;
}
// Insert
struct LNode* insertToHead(struct LNode* head, int v){
	struct LNode* newNode = (struct LNode*)malloc(sizeof(struct LNode*));
	newNode->ID = v;
	newNode->tieptheo = head;
	return newNode;
}
struct LNode* insertToLast(struct LNode* head, int v){
	struct LNode* newNode = createNode(v);	
	if(head == NULL){
		return insertToHead(head,v);
	}else{
		struct LNode* temp = head;	
		while(temp->tieptheo != NULL){
			temp = temp->tieptheo;
		}		
		temp->tieptheo = newNode;
		return head;
	}
}
// Delete
struct LNode* removeLast(struct LNode* head){
	struct LNode* prev = NULL;
	struct LNode* current = NULL;
	current = head;
	while(current->tieptheo != NULL){		
		prev = current;
		current = current->tieptheo;
	}
	prev->tieptheo = NULL;
	free(current);
	return head;
}
// HAM XOA DE QUY
// EM KHONG CO Y TUONG
struct LNode* loaibo(struct LNode* head){
	struct LNode* temp;
	if(temp->tieptheo == NULL){
		temp = NULL;
		return head;		 
	}else{
		temp = loaibo(head->tieptheo);
		head->tieptheo = temp;
		return head;
	}
}

void inDanhSach(struct LNode* head){
	struct LNode* temp = head;
	for(temp; temp!= NULL; temp = temp->tieptheo){
		printf("i: %d\n",temp->ID);
	}
}


int main(int argc, char *argv[]) {
	struct LNode* head = init();
	
	head = insertToLast(head, 1);
	head = insertToLast(head, 3);
	head = insertToLast(head, 3);
	head = insertToLast(head, 4);
	head = insertToLast(head, 8);
	head = insertToLast(head, 5);
	head = insertToLast(head, 15);
	
	inDanhSach(head);
	
	head = loaibo(head);
	printf("------ AFTER REMOVE LAST----- \n");
	inDanhSach(head);
	return 0;
}
1 Like

Ý tưởng à :thinking::thinking:

  • Trả về con trỏ đầu thì truyền head vào, khi xoá xong thì trả về head nguyên si như ban đầu.
  • Anh có thể sử dụng đệ quy giống như vòng lặp. Điều kiện trong if() là điều kiện dừng. Những câu lệnh khi thoả mãn điều kiện sẽ là những câu lệnh trong while. Còn lại bê từ while sang đệ quy và thay đổi 1 chút. Cố thử suy nghĩ và code xem nào :grin:
5 Likes

Mình viết giải thuật thế này bạn xem đc k.
tạo 1 hàm

 Node dequy (Node p){
     if(p.next =!NULL)  return dequy(p.next);
     else return NULL;
}

cho đối số lúc đầu là dequy(head);

//typedef struct LNode *Node;
3 Likes

Đoạn code trên không chạy bạn ạ

1 Like

Minh viết ý tưởng vậy thôi. Đó là giải thuật. Còn bạn rap vào project của bạn sao đó cho cú pháp nó hợp lí là được vì mình lâu rồi cũng không code trên C++ nên mình cũng k chắc cú pháp có đúng không, nhưng cơ bản ý tưởng là vậy.

2 Likes

Bạn có thể nói rõ hơn về giải thuật của bạn ko, mình chưa hiểu về ý tưởng đó. Cám ơn bạn

1 Like
struct LNode* loaibo(struct LNode* head , struct LNode* current ){
	if(current->tieptheo != 0){
        return loaibo(head, current->tieptheo);
	}
	else{
	    /*bla bla để xóa node đó */
        return head;
	}
}

Chuyên mục làm hỏng 1 thế hệ :disappointed:
Anh/ chị xem code trên thử xem. Nó là 1 dạng thay cho loop thôi. Không hiểu chỗ nào thì có thể hỏi lại ạ.
Lời gọi hàm : loaibo(head , head);

5 Likes

Chỉ có 1 con trỏ head thôi mà, ko có 2 con trỏ đâu :frowning:

Thì chỉ có 1 con trỏ head thôi mà. Trong hàm trên em có khai báo thêm 1 con trỏ head nữa đâu ạ.

2 Likes

Trả về chính tham số bạn truyền vào thôi.

5 Likes

hàm thế này mà:

struct LNode* loaibo(struct LNode* head)

, đâu có con trỏ LNode* current đâu :frowning:

Khi cậu viết đệ quy, cậu phải có trường hợp cơ sở :smile:

Với yêu cầu của cậu:

Tớ sẽ ví dụ cách để cậu suy luận, bằng cách đưa ra các câu hỏi và trả lời. It’s all about logic.

Q: Nếu node của cậu truyền vào là node cuối, cậu sẽ làm gì?
A: Free node đó và trả về null.
Đây là trường hợp cơ sở (1)

Q: Nếu node của cậu truyền vào là node gần cuối, cậu sẽ làm gì?
A: Free node tiếp theo của node đó (mục tiêu đầu tiên của cậu), và biến node đó thành node cuối bằng cách gán node->next = null, rồi return chính node đó (mục tiêu thứ 2 của cậu).
Đây là trường hợp cơ sở (2)

Q: Nếu node của cậu truyền vào là node gần gần cuối, cậu sẽ làm gì?
A: Cậu sẽ làm thao tác ở TH cơ sở (2) với node tiếp theo.
Tức là cậu sẽ gọi chính hàm này với node tiếp theo: loaibo(node->next); (3)

Giờ tổng hợp 3 dữ kiện trên, cậu sẽ có cài đặt dưới đây:

if(isEndNode(node)) { // Nếu là node cuối
   free(node); // Free node đó
   return NULL; // Trả về NULL
}

if(almostEndNode(node)) { // nếu là node gần cuối (cậu tự viết cài đặt của nó nhé)
    free(node->next); // Free node cuối (là node tiếp theo)
    node->next = null; // rồi gán node cuối thành null, tức là giờ node này là node cuối
    return node; // Return chính node đó
}

loaibo(node->next); // nếu không, loại bỏ node cuối của node tiếp theo
return node; // Return chính node đó, vì cậu cần return node đầu mà

Đó có thể là cài đặt của cậu, ít nhất cho tới node gần cuối :smiley:

Giờ, câu hỏi tiếp theo vô cùng quan trọng là:

Q: Nếu node của cậu truyền vào là node bất kỳ nào đó xa node cuối, cài đặt trên có thỏa mãn không
A: Có chứ. Thao tác loaibo(node->next) sẽ không dừng lại nếu trường hợp cơ bản không thỏa mãn. Cậu sẽ tiếp tục cùng thao tác cho tới khi gặp node cuối cùng.

Easy, right? :smiley:

Còn 1 câu hỏi nhí nhố nữa, để mọi thứ hoàn hảo:
Q: Nếu node là NULL, cậu sẽ làm gì?
A: Do nothing and return NULL.

Tớ nghĩ là cậu có thể cài đặt câu hỏi trên mà không gặp vấn đề gì :smiley:


Đó là cách cậu suy luận khi cài đặt bất cứ hàm đệ quy nào: Tìm trường hợp cơ sở từ việc đặt câu hỏi dễ nhất, rồi đi dần tới các câu hỏi phức tạp hơn, và giải quyết từng câu hỏi đó.
Trong thực tế, việc đặt câu hỏi này sẽ được thay thế bằng việc viết test case, và việc trả lời câu hỏi sẽ được thay thế bằng implement để thỏa mãn test case đó.
Đúng rồi, tớ đang nói tới test-driven development (TDD) đấy! :smiley:

Hi vọng sau khi đọc hướng dẫn trên, cậu có thể tự tìm cách design bất kỳ hàm đệ quy nào, mà không bị tuyệt vọng như thế này:

6 Likes

Thật là không có gì bằng những lời gợi ý, hướng dẫn, giải thích tuyệt vời của bác, em rất cảm ơn. Em sẽ nghiên cứu thật kỹ những lời bác nói. Và sẽ hi vọng sẽ như lời bác nói, không có gì là tuyệt vọng khi dính vào đệ quy. Cá nhân em cũng khá yêu thích đệ quy vì tính logic, tính ngắn gọn của đệ quy.
Một lần nữa cảm ơn bác
Dựa vào hướng dẫn của bác, em viết nốt những gì còn lại theo cách của em.

   struct LNode* loaibo(struct LNode* head){
	if(head == NULL){
		return NULL;
	}
	if(head->tieptheo == NULL){
		head = NULL;
		free(head);
		return NULL;
	}
	if(head->tieptheo->tieptheo == NULL){
		head->tieptheo = NULL;	
		free(head->tieptheo);		
		return head;
	}
	loaibo(head->tieptheo);
	return head;
}

@ Code edited

2 Likes

Cảm ơn cậu, về kind words :smile:

Cậu nhớ test cẩn thận nhé, tớ nghĩ code đó có vài lỗi đấy. Cậu sẽ sớm tìm được lỗi khi test thôi. Tớ sẽ không spoil gì, để cậu tận hưởng việc này :smiley:
Lúc nào sửa xong, cậu có thể edit lại comment trên, just for future reference.

4 Likes

Có điều này em muốn hỏi.

if(almostEndNode(node)) { // nếu là node gần cuối (cậu tự viết cài đặt của nó nhé)
    free(node->next); // Free node cuối (là node tiếp theo)
    node->next = null; // rồi gán node cuối thành null, tức là giờ node này là node cuối
    return node; // Return chính node đó
}

Về việc giải phóng node trong danh sách.

  • Có cần thiết phải gọi hàm free(node->next) hay không? Hay chỉ cần gọi câu lệnh
    node->next=NULL là đủ
  • Tại sao khi gọi free(node->next) mà không gọi node->next=NULL; là chương trình lỗi
    -> Mục đích và cách dùng của free(node->next) là gì. Giải đáp giúp em nhé

Về các câu hỏi của cậu:

Cậu phải gọi free, bởi cậu sử dụng malloc - cấp phát động - để tạo ra node, nên khi xóa node đi, cậu phải có trách nhiệm giải phóng bộ nhớ (C hay Cpp không có garbage collector để dọn dẹp heap memory).
Nếu không giải phóng bộ nhớ, các giá trị đó sẽ không bị mất đi (trừ khi cậu restart máy), và dần dần cậu sẽ hết sạch bộ nhớ.
Đây là basic operation khi cậu làm việc với C hay Cpp, nên tớ khuyên cậu nên làm theo luật đó. Cậu có thể bị cộng đồng những người yêu C/Cpp trục xuất lên tàu cá nào đó để thái hành trong 6 tháng nếu cậu không làm :smile:

Tớ có giải thích lý do ở trên rồi. Mục đích của free là để giải phóng bộ nhớ heap khi cậu khởi tạo giá trị sử dụng malloc/calloc/ realloc.
Reference: http://www.cplusplus.com/reference/cstdlib/free/

Cậu có thể tham khảo luôn cách dùng trên reference mà tớ đính kèm ở trên.

5 Likes

Cám ơn Giáo sư.

Comment chất, Avatar cũng rất chất.
@ Tiếc quá, kế hoạch sắp thành công lại bị bắt, không biết còn phần tiếp theo nữa không.

1 Like
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?