• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Struktura linked list - usuwanie węzłów z wartością wyższą niż średnia co n-tego elementu

Object Storage Arubacloud
+1 głos
396 wizyt
pytanie zadane 26 maja 2018 w C i C++ przez Adam Jakś Dyskutant (8,940 p.)

Hej, mam takie polecenie dotyczące struktury linked list (sznur):

zaprojektuj funkcję usun usuwającą ze sznura wszystkie elementy o wartościach większych niż średnia z wartości sznura znajdujących się w co M-tym elemencie tego sznura począwszy od wartości na pozycji M (M jest liczbą całkowitą dodatnią będącą parametrem funkcji, w przypadku gdy brak jest elementów do obliczenia powyższej średniej, sznur pozostawiany jest bez zmiany). Pamięć zajmowana przez usuwane elementy ma zostać zwolniona.

i próbowałem to rozwiązać w sposób jak poniżej:

void deleteM(int m) {
        node *current = new node;

        int i = 1;
        int eli = 0;
        int sum = 0;

        current = head;

        while (current -> next != NULL) {
            current = current -> next;
            i++;
            if (i%m == 0) {
                eli++;
                sum += current -> data;
            }
        }

        current = head;
        node *previous = new node;

        while (current != NULL) {

            if (current -> data > sum/eli) {
                previous = current;
            }

            previous -> next = current -> next;
            current = current -> next;
        }

    }

Ale program się buguje (pierwsze kilka węzłów się nie usuwa) jeśli wartość data dla head jest większa niż średnia. Przy okazji będę wdzięczny za porady na temat refaktoryzacjii mojego roboczego obecnie kodu.

Cały kod:

#ifndef KOLOKWIUM_H_INCLUDED
#define KOLOKWIUM_H_INCLUDED

using namespace std;

struct node {
    int data;
    node *next;
};

class List {
private:
    node *head, *tail;
public:
    List() {
        head = NULL;
        tail = NULL;
    }

    void print() {
        node *temp = new node;
        temp = head;
        while (temp != NULL) {
            cout << temp -> data << "\t";
            temp = temp -> next;
        }
        cout << endl;
    }

    void insertEnd(int value) {
        node *temp = new node;
        temp -> data = value;
        temp -> next = NULL;
        if (head == NULL) {
            head = temp;
            tail = temp;
        } else {
            tail -> next = temp;
            tail = temp;
        }
    }

    void deleteM(int m) {
        node *current = new node;

        int i = 1;
        int eli = 0;
        int sum = 0;

        current = head;

        while (current -> next != NULL) {
            current = current -> next;
            i++;
            if (i%m == 0) {
                eli++;
                sum += current -> data;
            }
        }

        current = head;
        node *previous = new node;

        while (current != NULL) {

            if (current -> data > sum/eli) {
                previous = current;
            }

            previous -> next = current -> next;
            current = current -> next;
        }

    }
};
#endif // KOLOKWIUM_H_INCLUDED

Dzięki :)

1 odpowiedź

+1 głos
odpowiedź 26 maja 2018 przez RafalS VIP (122,820 p.)
wybrane 27 maja 2018 przez Adam Jakś
 
Najlepsza

Po pierwsze, nie rozumiem czemu zawsze robisz tak:

		node *temp = new node;
		temp = head;

gdyż jest to po prostu sztandarowy przykład wycieku pamięci. Alokujesz node na stercie i w kolejnej linijce zapominasz wskaźnik, przez co już tego node nie zwolnisz, bo go zapomniałeś. Jak już chcesz tego tempa czymś zainicjować to proponuje nieszkodliwy null_ptr czy tam NULL.
 

int i = 1;
...
i++;
if(i%m == 0)

Czemu zaczynasz od drugiego elementu w tak dziwny sposób? Czemu wgl zaczynasz od drugiego? Jeśli ktoś poda deleteM(1) to pominiesz pierwszy element.

if (current->data > sum / eli)

Dzielenie int przez int obcina końcówkę. Nie jest to porządane np w sytuacji 2 < 5/2 gdzie wyjdzie fałsz mimo ze to prawda.

        while (current != NULL) {
 
            if (current -> data > sum/eli) {
                previous = current;
            }
 
            previous -> next = current -> next;
            current = current -> next;
        }

Prawdziwy błąd jest tutaj. Wyobraź sobie taką listę: 1,50,1,1,1,1,1,1,1,1. Wywołujemy deleteM(2). W drugim obiegu pętli ustawia wskaźnik previous na drugi element listy i już do samego końca nie wchodzi do ifa, więc przypisuje do drugiElementListy->next kolejne elementy. Tak do samego końca czyli NULLa. Więc efektywnie uciąłeś liste za drugim elementem. 

No i nigdzie nie zwalniasz pamięci. Każdemu new musi odpowiadać delete (przynajmniej jeśli mówimy o surowych wskaźnikach). U Ciebie nie widziałem delete nigdzie. Ale to pewnie dlatego, że to wersja robocza.

Co do refaktoryzacji to nazwa zmiennej eli nic nie mówi. Wydaje mi się, że w nienaturalny sposób napisałeś tą pętlę:
 

        while (current -> next != NULL) {
            current = current -> next;
            i++;
            if (i%m == 0) {
                eli++;
                sum += current -> data;
            }
        }

Chodzi o to, że jako programiści wszyscy przywykliśmy do sekwencji iteracji z pętli for. Tzn: sprawdzasz warunek, jesli prawdziwy wykonujesz ciało i dopiero potem robisz i++, czy tam przesuwasz wskaźnik. U ciebie nie wiadomo czemu ten iterator jest zwiększany na samym poczatku. W ten sam sposób przesuwany jest current. Może akurat wskaźnik był przesuwany celowo na początku, bo przez to pomijałeś pierwszy element (celowo lub nie), ale miejsce i++ nie miało znaczenia. Tak czy siak wg mnie jest to niepotrzebne odstępstwo od standardu, do którego wszyscy przywykliśmy i który jest logiczniejszy. 
Gdybyś faktycznie chciał pominąć pierwszy to trzeba było przed pętlą zrobić current = head->next i już, wszystko czytelne jak na dłoni - startujemy od drugiego elementu.

Co do samego rozwiązania - gratuluje, bo chyba był to najtrudniejszy algorytm na listach z jakim spotkałem się na tym forum :D. Tak ja widzę rozwiązanie
 

#include <iostream>
using namespace std;

struct node {
	int data;
	node *next;
};

class List {
private:
	node * head, *tail;
	double MthElementsAverage(int m) {
		//przyjalem, ze elementy listy indeksujemy od 1
		//bo nie bylo sprecyzowane
		int i = 1;
		int mthElementsCounter = 0;
		int sum = 0;

		node *current = head;

		while (current != NULL) {
			if (i%m == 0) {
				mthElementsCounter++;
				sum += current->data;
			}
			current = current->next;
			i++;
		}
		return sum / (double)mthElementsCounter;
	}
	void deleteNext(node* toDeleteAfter) {
		if (toDeleteAfter->next == NULL)
			return;
		else {
			node* oneAfterDeleted = toDeleteAfter->next->next;
			delete toDeleteAfter->next;
			toDeleteAfter->next = oneAfterDeleted;
		}
	}

	void deleteFromBeggining() {
		node* oneAfterHead = head->next;
		delete head;
		head = oneAfterHead;
	}

	void deleteNodesGreaterThanThreshold(double threshold) {
		if (head == NULL)
			return;

		node *current = head;
		node *previous = NULL;

		while (current != NULL) {
			if (current->data > threshold) {
				//przypadek usuwania z poczatku jest inny
				//bo trzeba przestawic head
				if (current == head) {
					deleteFromBeggining();
					current = head;
				}
				//przypadek gdy jestesmy gdzies w srodku listy
				else {
					//jesli weszlismy tu, to znaczy ze co najmniej jednego
					//node minelismy, a to znaczy, ze previous jest ustawiony
					//i nie jest nullem, wiec nie trzeba sprawdzac
					deleteNext(previous);
					current = previous->next;
				}
			}
			else {
				//zwykle przesuniecie
				previous = current;
				current = current->next;
			}
		}
	}
public:
	List() {
		head = NULL;
		tail = NULL;
	}

	void print() {
		node *temp = new node;
		temp = head;
		while (temp != NULL) {
			cout << temp->data << "\t";
			temp = temp->next;
		}
		cout << endl;
	}

	void insertEnd(int value) {
		node *temp = new node;
		temp->data = value;
		temp->next = NULL;
		if (head == NULL) {
			head = temp;
			tail = temp;
		}
		else {
			tail->next = temp;
			tail = temp;
		}
	}

	void deleteM(int m) {
		deleteNodesGreaterThanThreshold(MthElementsAverage(m));
	}
};

int main()
{
	List l;
	//test wielokrotnego usunięcia z poczatku
	l.insertEnd(50);
	l.insertEnd(50);
	l.insertEnd(1);
	//standardowy test usunięcia ze środka
	l.insertEnd(50);
	l.insertEnd(2);
	l.insertEnd(3);
	l.insertEnd(4);
	//usunięcie kilku pod rzad ze srodka
	l.insertEnd(50);
	l.insertEnd(50);
	l.insertEnd(5);
	//usuniecie dwoch z konca
	l.insertEnd(50);
	l.insertEnd(50);
	l.print();
	l.deleteM(2);
	l.print();
}

Przy okazji dodałem kolejny element refaktoryzacji, który do Twojego kodu też bym zastosował. Jest taki zbiór 5 zasad pisania solidnego kodu (SOLID) i pierwsza mówi, że funkcja powinna mieć jedną odpowiedzialność. Co generalnie w wielkim uproszczeniu streszcza się do robienia wielu małych funkcji. Co na tym zyskujemy: np 

double average = MthElementsAverage(m)

i od razu wiemy czym jest average, gdyby nie było tu wywołania funkcji to osoba czytająca kod musiałaby przeanalizować pętlę i sama dojść do tego, że pierwsza połowa metody deleteM liczy średnią, a tak to ma ten kawałek kodu nazwany w postaci funkcji i kod jest o wiele czytelniejszy. Zauważ, że metody pomocnicze zrobiłem prywatne. Robi się tak, żeby na zewnątrz klasa pozostała taka sama - tzn żeby jej zestaw publicznych metod (interfejs) się nie zmienił, gdyż poszatkowanie funkcjonalności której od klasy wymagamy na zewnątrz jest jej szczegółem implementacyjnym.


 

komentarz 27 maja 2018 przez Adam Jakś Dyskutant (8,940 p.)

Po pierwsze, nie rozumiem czemu zawsze robisz tak [...]

 Zmienna head również jest wskaźnikiem i deklaruję ją wcześniej, w strukturze. Przypisuję nowy węzeł do head, ponieważ później wykorzystuję ten mechanizm do iterowania po elementach listy zaczynając od pierwszego.

Dziękuję za zwrócenie uwagi na kwestie inkrementacji i i typu zmiennej dla sumy.

W tej ostatniej pętli próbuję usunąć element listy jeśli jego data jest większa od średniej. Spróbowałem dopasować tutaj mechanizm usuwania elementu listy z dowolnej pozycji (https://www.codementor.io/codementorteam/a-comprehensive-guide-to-implementation-of-singly-linked-list-using-c_plus_plus-ondlm5azr#deletion-at-a-particular-position), tylko zamiast inta określającego pozycję podawaną przez użytkownika, chciałem zautomatyzować ten proces za pomocą while'a.

komentarz 27 maja 2018 przez RafalS VIP (122,820 p.)
Zaktualizowałem troche post, bo wczoraj wieczorem nie chciało mi się go kończyć. Rzuć okiem
komentarz 27 maja 2018 przez Adam Jakś Dyskutant (8,940 p.)
Dzięki za odpowiedź i kompleksowe wytłumaczenie tematu. Bardzo mi pomogłeś.

Ten algorytm to dopiero początek :) W linku więcej tego typu ciekawych zadań, dla zainteresowanych:

http://www.math.uni.lodz.pl/~cybula/psd/1617l-prstr-kol.pdf
komentarz 27 maja 2018 przez RafalS VIP (122,820 p.)

Zmienna head również jest wskaźnikiem i deklaruję ją wcześniej, w strukturze. 

Zgadzam się, ale

node *temp = new node;
temp = head;

robi jeszcze coś więcej. Pierwsza linijka: tworzymy wskaźnik na node oraz przypisujemy do niego nowy obiekt typu node zaalokowany na stercie, który w drugiej linijce jest nadpisywany przez obiekt node, na który wskazuje head. A to oznacza, że zapomnieliśmy gdzie zaalokowaliśmy wcześniejszego node. A jeśli zapomnieliśmy to już tej pamięci nie odzyskamy przed zakończeniem programu, czyli mamy wyciek pamięci. Jeśli nie wierzysz możesz puścić takiego maina:
 

    node *head = new node;
    for (size_t i = 0; i<20000000; i++)
    {
        node *current = new node;
        current = head;
    }

U mnie ten proces zajmował zjadał troszke ponad GB ramu :P

komentarz 27 maja 2018 przez RafalS VIP (122,820 p.)

Żeby pokazać bardziej realistyczny wyciek pamięci trzeba najpierw dodać destruktor:

	~List() {
		node *tmp;
		while (head != NULL) {
			tmp = head->next;
			delete head;
			head = tmp;
		}
	}

I przetestować dwie wersje naszej listy:

W metodzie MthElementsAverage zrobić tak jak Ty robiłeś:

		node *current= new node;
		current = head;

i puścić maina, który będzie symulował wielokrotne używanie Twojej listy w jednym programie:
 

	for (size_t i = 0; i<20000000; i++)
	{
		List l;
		l.insertEnd(1);
		l.deleteM(2);
	}

I program zjada 1 GB pamięci. A potem zmieniamy felerną linijkę:

		node *current;// = new node;
		current = head;

puszczamy tego samego maina i wuala:
program zjada 880 KB.

Mam nadzieje, że już rozumiesz czemu wycieki są złe. Jeśli program ma działać długo jak  np przeglądarka internetowa to jeśli ma jakiś wyciek pamięci to będzie zajmował niepotrzebnie dużo ramu, a to jest krótko mówiąc niefajne. Ale generalnie resetujemy przeglądarkę i przez chwile wszystko jest cacy. A teraz wyobraź sobie, że program z wyciekiem steruje reaktorem jądrowym :D

Podobne pytania

0 głosów
1 odpowiedź 121 wizyt
pytanie zadane 1 lipca 2020 w Java przez amtrax Dyskutant (9,630 p.)
0 głosów
1 odpowiedź 1,587 wizyt
0 głosów
0 odpowiedzi 530 wizyt
pytanie zadane 12 września 2020 w C i C++ przez magda_19 Gaduła (3,080 p.)

92,556 zapytań

141,404 odpowiedzi

319,562 komentarzy

61,942 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Kolejna edycja największej imprezy hakerskiej w Polsce, czyli Mega Sekurak Hacking Party odbędzie się już 20 maja 2024r. Z tej okazji mamy dla Was kod: pasjamshp - jeżeli wpiszecie go w koszyku, to wówczas otrzymacie 40% zniżki na bilet w wersji standard!

Więcej informacji na temat imprezy znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...