Frage zu folgender Aufgabe, ob man O(log(k)*n) argumentieren kann

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
divi
User
Beiträge: 3
Registriert: Montag 5. September 2022, 17:23

Hey,
ich habe eine Frage zu einer Aufgabe, in der es darum geht, das k-größte Element einer unsortierten Liste in der Laufzeit O(n*log(k)) zurückzugeben, also das k = 1 größte Element ist das größte Element, das k = 2 größte Element ist das zweitgrößte, etc.
In der Aufgabe stand zwar etwas davon, man könne Min/Max-Heaps verwenden, oder mehrfache Heaps, und dass die Laufzeit O(n + k*Log(k)) (=O(n)?) auch akzeptabel sei. Ich habe versucht das ganz über Radix-LSD zu lösen, schicke hier einfach mal die def rein. Nun frage ich mich nur im Nachhinein, ob das innerhalb der besagten Komplexität wäre? Außerdem würde ich auch gerne wissen, welche Lösung mit Heaps richtig gewesen wäre? Ich habe ursprünglich versucht die Liste zu einem Heap zu überführen (also einen Heapanteil definieren durch eine Laufvariable i, die von len(l) bis 0 jeweils das Element solange an die (i-1)//2 te Stelle tauscht, bis das (i-1)//2-te Element entweder größer ist, oder man am ersten Index ist. Dadurch würde sich dann ein Max Heap ergeben in O(n), von dem man dann das größte Element extrahiert, also immer das an 0ter Stelle und dann das letzte an die erste Stelle tauscht und heapify Down darauf aufruft (log(n)). Das ganze k mal, dann hat man das k-größte Element. Wäre dann halt eher in O(k*log(n)). Deswegen habe ich es über Radix probiert:

Code: Alles auswählen

def k_greates_elem(l,k):
    
    def radix(l): #Gewünschte Laufzeit ist n*log(k), hier durch Radix Sort
        
        iter = 1
        goon = True
        
        while goon:
            
            lSol = []
            for _ in range(10): #erstmal wird die Liste in die Buckets von 1-9 eingeteilt, dafür nehme ich eine Hilfsliste lSol
                lSol += [[]]
            if goon:
                abbr = False
            for elem in l:
                if elem > (10 * iter): #Hier wird auch gleich nach der Abbruchbedingung geguckt, ob kein Element größer als das Modulo ist
                    
                    abbr = True
            
                lSol[elem % (10 * iter)] += [elem]  #Das Einsortieren erfolgt nach Index. Dies passiert in n Schritten pro Iteration. Da der Modulo jedes mal /10 geteilt wird
            if not abbr:                            # wird dies log(k) (log Base 10) mal wiederholt
                
                goon = False
            iter *= 10
            l = []
            
            for h in lSol:                          #der Einfachheit halber wandel ich jedes mal wieder zur "normalen" Liste um, dann wieder zu lSol usw.
                l += h
            
            
        return l
    l = radix(l)                                    #sortierte Liste, das k-größte Element ist das an der len(l) - k-ten Stelle. Die Zugriffszeit auf Listenindexe ist konstant
    return l[len(l)-k]                              #Also ist der Algorithmus aus O(n*log(k)) (Base 10)

l = [3,4,1,9,2,5]
print(k_greates_elem(l,5))
Feedback wäre super, vielen Dank
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Kommentare sollten nich am Ende einer Zeile stehen, und auch nicht irgendwo außerhalb des rechten Randes, denn so kann man die Kommentare nicht lesen, was kontraproduktiv ist.
Gute Variablennamen sind wichtig. Ich weiß nicht was man mit einem Flag `Blödmann` anfängt.
`el sol` heißt die Sonne, was das aber in Deinem Programm bedeutet, verstehe ich nicht.
Wenn man ein Element an eine Liste anhängen will, benutzt man `append` und erzeugt keine Einelementige Liste.
Der Kommentar über die Buckets 1-9 ist falsch, da sie von 0 ab laufen.

Code: Alles auswählen

def k_greates_elem(values, k):
    def radix(values):
        # Gewünschte Laufzeit ist n*log(k), hier durch Radix Sort
        iter = 10
        while True:
            # erstmal wird die Liste in die Buckets von 0-9 eingeteilt,
            # dafür nehme ich eine Hilfsliste lSol
            buckets = [[] for _ in range(10)]

            abbr = False
            for value in values:
                # Hier wird auch gleich nach der Abbruchbedingung geguckt,
                # ob kein Element größer als das Modulo ist
                if value > iter:                    
                    abbr = True
            
                # Das Einsortieren erfolgt nach Index. Dies passiert in n Schritten
                # pro Iteration. Da der Modulo jedes mal /10 geteilt wird
                buckets[value % iter].append(elem)
            
            # der Einfachheit halber wandel ich jedes mal wieder zur "normalen"
            # Liste um, dann wieder zu lSol usw.
            values = []
            for bucket in buckets:
                values += bucket

            if not abbr:
                # wird dies log(k) (log Base 10) mal wiederholt
                break
            iter *= 10
        return values

    # sortierte Liste, das k-größte Element ist das an der len(l) - k-ten Stelle.
    # Die Zugriffszeit auf Listenindexe ist konstant
    values = radix(values)                                    
    # Also ist der Algorithmus aus O(n*log(k)) (Base 10)
    return values[-k]

l = [3,4,1,9,2,5]
print(k_greates_elem(l,5))
Das größte Problem am Algorithmus ist, dass er nicht funktioniert. Schau Dir nochmal genau an, wie radix-sort funktioniert.
Und wenn er funktionieren würde, würdest Du erst die Daten komplett sortieren, um danach das k-t größte Element rauszugeben. Dafür gibt es aber bessere Sortieralgorithmen:

Code: Alles auswählen

def k_greates_elem(values, k):
    return sorted(values)[-k]
So ist die Aufgabe aber nicht gedacht.
Heap-Sort besteht aus zwei Teilen, wobei der erste mit O(n) und der zweite mit O(n*log(n)) läuft, wobei der zweite Teil nur k mal gemacht werden muß um den k-t größten Wert zu finden.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@divi: Was ein bisschen abschreckt sind die schlechten Namen und die Kommentare die das schwer lesbar bis unlesbar machen wenn man sich das in einer Umgebung anschaut die zum Beispiel bei 80 Zeichen umbricht. Die längste Zeile ist 175 Zeichen lang.

Gewöhn Dir komische kryptische Abkürzungen gar nicht erst an, oder falls es dafür schon zu spät ist, schnell wieder ab. `l` ist kein sinnvoller Name und auch leicht mit 1 zu verwechseln in vielen Schriftarten. `goon` hat im Englischen eine Bedeutung. Hier war vielleicht `go_on` gemeint.

Bei `k_greates_elem` fehlt ein `t` und das `ent` am Ende muss man sich wirklich nicht sparen. Dann währe `k_greatest_element` ein guter Name für das k-grösste Element. Das ist aber keine Tätigkeit und damit kein so guter Name für eine Funktion. Bei `radix` ähnlich. Guter Name für eine Zahl die als Basis verwendet wird, kein guter Name für eine Sortierfunktion.

Funktionen sollte man nicht verschachteln, sofern es nicht wirklich kleine Hilfsfunktionen sind, die man nie testen und von Fehlern bereinigen muss, oder man tatsächlich ein Closure braucht/schreiben will.

``l[len(l)-k]`` ist das gleiche wie ``l[-k]``, dann brauchst Du `l` überhaupt nicht, weil Du direkt auf das Ergeignis vom Sortieren zugreifen kannst.

`iter` ist der Name einer eingebauten Funktion, den sollte man nicht an etwas anderes binden. Der Name beschreibt auch nicht wirklich was der Wert bedeutet.

Das erste ``if goon:`` macht offensichtlich keinen Sinn weil das an der Stelle immer wahr ist, denn sonst wäre die ``while``-Schleife ja abgebrochen worden.

`lSol` hat ein grosses `S` was da nicht hingehört. Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase). Und was hat die Sonne mit den Elementen dieser Liste zu tun? Der Name ist schlecht. Statt in einem Kommentar zu schreiben, das das eine Hilfsliste ist, die Buckets enthält, sollte die Liste vielleicht `buckets` heissen.

``+=`` ist zum erweitern einer Liste mit Element*en* — Mehrzahl!. Um *ein* Element an eine Liste anzuhängen steckt man das nicht unnötig in eine Liste die man eigentlich nicht braucht und die nach ``+=`` dann wieder weggeworfen wird, nach dem das eine Element aus der Liste in die andere Liste kopiert wurde. Zum Anhängen von einzelnen Elementen gibt es die `append()`-Methode. Und selbst wenn man den Effekt von ``+=`` haben will, würde ich ausserhalb von Code-Golf auch die `extend()`-Methode verwenden. Das ist klarer und weniger verwirrend, weil ``+=`` bei Listen nicht das gleiche macht die ``=`` und ``+`` kombiniert.

`abbr` und `goon` sind ja irgendwie komplementär. Warum gibt es beides?

Hier mal ein Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
from itertools import chain


def radix_sort(items):
    factor = 1
    keep_running = True
    while keep_running:
        buckets = [[] for _ in range(10)]

        keep_running = False
        for item in items:
            keep_running = keep_running or item > (10 * factor)
            buckets[item % (10 * factor)].append(item)

        factor *= 10
        items = list(chain.from_iterable(buckets))

    return items


def get_k_greatest_element(items, k):
    #
    # Das k-größte Element ist das an der len(l) - k-ten Stelle. Die
    # Zugriffszeit auf Listenindexe ist konstant, also ist der Algorithmus aus
    # O(n*log(k)) (Base 10)
    #
    return radix_sort(items)[-k]


def main():
    numbers = [3, 4, 1, 9, 2, 50]
    print(get_k_greatest_element(numbers, 5))


if __name__ == "__main__":
    main()
Der auf die Nase fällt bei der 50 in den Zahlen, weil Dein Radix-Sort so offensichtlich nicht funktioniert.

Das würde wenn die Laufzeit passen würde ja auch nur für Zahlen funktionieren. Der Heap-Ansatz funktioniert für alles was sich vergleichen lässt, nicht nur für Zahlen.

Ich würde es so lösen 😛:

Code: Alles auswählen

#!/usr/bin/env python3
from heapq import nlargest


def get_k_greatest_element(items, k):
    return nlargest(k, items)[-1]


def main():
    numbers = [3, 4, 1, 9, 2, 50]
    print(get_k_greatest_element(numbers, 5))


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
divi
User
Beiträge: 3
Registriert: Montag 5. September 2022, 17:23

Ok, danke für die Antworten. Auf meinem Bildschirm passt neben dem Explorer und einer Übersicht auch ein String der Länge 175 in eine Zeile. Vielleicht erkläre ich, was genau die Bedeutung der Variablen sein soll. Iter ist eine Kurzform für Iteration, da dieser Wert angibt, in welcher Iteration man sich befindet, wenn man ihn mit dem Logarithmus zur Basis 10 bildet. goon wurde schon richtig erkannt als "go on" boolean, ohne Unterstrich schreibt es sich für mich einfacher und schneller, mir war nicht bewusst, dass solche Nameskonventionen, seien sie nun von Java oder Python so sehr zur Leserlichkeit von Code beitragen. Auch lSol ist eine Abkürzung, l für Liste und Sol für Solution. Also die Rückgabeliste. Alternativ verwende ich auch retL für "return List". Mir war bewusst, dass die Buckets bei 0 anfangen und so gesehen der 11te überflüssig ist, fand persönlich aber schöner eine Liste mit 11 Unterlisten zu haben als 10. Abbr scheint ja korrekt verstanden worden zu sein. Diese Variable habe ich deswegen benutzt, da man beim Radix-LSD-Sort eine Abbruchbedingung braucht, welche ich darauf festsetzen wollte, wenn keins der Elemente größer ist, als das Modulo. Am einfachsten fiel mir das Umzusetzen, indem ich vor jedem Durchlauf die Variable auf False gesetzt habe, so dass diese nur dann am Ende auf True wäre, wenn im letzten Durchlauf goon schon False wäre. Dass es Do-While in Python gibt wusste ich nicht. Leider haben wir für die Aufgabe etwas strengere Richtlinien, die es u.a. verbieten Code zu benutzen/ einzubinden per import oder wie auch immer, den wir noch nicht hatten, oder durch den die Aufgabe trivial würde. Meine erste Funktion mit einem Heap Ansatz habe ich k_greatest_element(l,k) genannt und wollte ich nicht auskommentieren, daher habe ich hier die kürzere Form verwendet. Den += Operator auf Listen anzuwenden habe ich mir letztes Jahr angewöhnt, da wir zu dem Zeitpunkt .append oder .extend nicht nutzen durften. Unterfunktionen nutze ich eigentlich ganz gerne, wenn ein Algorithmus mehrere Aufgaben erfüllen soll, denn dann kann ich dies einfach Aufrufen, statt den Codeblock neu reinzupasten, was der Leserlichkeit auch nicht hilft. Außerdem grenzt es klar ab, wo was stattfindet und hilft mir zumindest beim Debugging. Was die Funktionalität angeht, so wird die Liste doch sortiert und das k-größte Element zurückgegeben? Ich bedanke mich für eure Lösungsvorschläge, leider gehen diese jedoch nicht weit genug. Die Liste durch einen importierten Sortiertalgorithmus zu sortieren reicht nicht aus, es geht darum, ohne etwas zu nutzen, dass die Aufgabe trivial machen würde, diese zu lösen. Falls jemand weiß, wie genau das mit einer Heap Implementierung (oder auch anders) gehen soll, würde ich mich über eine Antwort freuen, ein Lösungsweg würde mir auch schon reichen, oder ein Ansatz, den ich dann zuende denken kann. Wie du schon sagtest, wäre der Heap Ansatz nicht effizient genug, da man n*log(n) Schritte k mal ausführt also sogar n*k*log(n). Der Radix Sort ist mir deswegen eingefallen, weil er als ein Algorithmus gilt, der armortisiert in Theta(n) liegt, allerdings Umsetzen kann ich ihn effektiv nur in O(log(n)*n). Was wiederrum auch der Laufzeitanforderung von O(log(k)*n) nicht entspricht.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@divi: Das wird theoretisch bei den meisten auf den Bildschirm passen, aber eben nicht im Browser, im Terminal, im E-Mail-Client, in der IDE die noch tausend andere Anzeigen im Fenster unterbringt. Und ausserdem ist das selbst wenn es passt schwerer zu lesen je länger die Zeilen werden. Bei normalen Text ist die Faustregel 60 bis 65 Zeichen pro Zeile bevor es zu lang wird, das man den nächsten Zeilenanfang leicht findet. Für Quelltext ist das etwas entspannter, weil der weniger ”dicht” ist, aber die guten alten 80 Zeichen sind schon ganz sinnvoll gewählt und auch an vielen Stellen noch entweder Voreinstellung oder tatsächlich fest vorgegeben.

`iter` steht für `iteration` wenn man den Logarithmus zur Basis 10 bildet. Da kommt ja jeder sofort drauf wenn er `iter` liest. 😀 Wie gesagt, Python-Programmierer denken da als erstes an die eingebaute `iter()`-Funktion mit der man von iterierbaren Objekten einen Iterator bekommen kann.

Naja `goon` hat halt eine Bedeutung. Ich habe gleich an Schlägertypen gedacht und erst auf den zweiten Blick an `go_on`. Und was heisst hier tippen? Man muss das ja nur einmal tippen, danach kannst Du ja „goon“ oder „go“ oder „gn“ oder so tippen, dann wird das ja hoffentlich von der Autovervollständigung vorgeschlagen. Schlechte Namen, weil kryptisch abgekürzt, konnte man früher noch durch einfache Editoren und Tipparbeit rechtfertigen — heute geht das nicht mehr.

Grunddatentypen haben in Namen nichts zu suchen, die ändert man während der Entwicklung gar nicht so selten, und dann hat man falsche, irreführende Namen im Quelltext, oder muss alle betroffenen Namen suchen und ändern. Macht nur Arbeit.

Die Erklärungen für den falschen Kommentar für die Bucket-Liste werden nicht besser. Dir ist bewusst das die 11. Liste überflüssig ist? Welche 11. Liste? Da sind nur 10. Der Punkt ist, das falsche Kommentare schlimmer sind als gar keine Kommentare, denn Kommentare sollen Fragen klären, die beim lesen des Codes auftreten könnten. Wenn die dem Code widersprechen oder zumindest nicht dazu passen, dann weiss der Leser nicht was falsch ist: Der Code oder der Kommentar. Und dann erreicht der Kommentar genau das Gegenteil von dem wofür er eigentlich gedacht ist.

Genau `abbr` habe ich als Abkürzung verstanden. Stand doch für `abbreviation` oder? 🤔

Um das noch mal klar zu formulierebn: Es ist egal ob man die Abkürzungen versteht, man muss darüber nachdenken, und kann sich dabei irren, also sollte man das lassen, weil die ganzen Abkürzungen nicht wirklich einen Gewinn bringen, aber eben Reibungsverluste beim Lesen. Code *wesentlich* öfter gelesen und muss verstanden werden, als das er geschrieben wird. Deshalb sollte man den auf leichte Lesbarkeit optimieren, und nicht darauf, das man Buchstaben beim Tippen einspart.

Verschachtelte Funktionen helfen sicher nicht bei der Fehlersuche. Im Gegenteil. Man kann die Funktion nicht separat testen, und es besteht immer die Gefahr, dass man lesend auf Variablen aus der umgebenden Funktion zugreift, auch wenn man da eigentlich eine lokale Variable für einführen wollte.

Die Liste sollte halt *nicht* sortiert werden. Jedenfalls nicht vollständig. Und meine Lösung könnte Dir schon helfen. Ich importiere da das `heapq`-Modul aus der Standardbibliothek. Das ist in Python geschrieben und ganz gut kommentiert.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
divi
User
Beiträge: 3
Registriert: Montag 5. September 2022, 17:23

Naja, bei mir passt es auch im Browser in die Zeile, aber ist ja auch egal. Die iter() funktion kannte ich nicht, manchmal ist es mir einfach lieber, den Namen meiner Variable runterzuschreiben, als erstmal durch 10 andere Vorschläge durchzuscrollen, um dann das per autovervollständigung zu machen. Abbr steht für abbrechen oder Abbruch :D
Äh ja sorry hab mich verzählt, also dass der 10te Bucket überflüssig ist. Mir hilft es mit verschachtelten Funktionen fürs debugging. Zur Heapq Bib, dann erkläre mir bitte, was genau da passiert?
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

@digi: woher soll der Leser wissen, warum das eine k_greatest_element und das andere k_greatest_elem heißt? Wenn Du unterschiedliche Algorithmen implementieren willst, dann wäre das offensichtlichste, den Unterschied im Namen zu haben k_greatest_element_with_heap_sort und k_greatest_element_with_radix_sort.
Es ist ein Unterschied zwischen, ob man zeigen soll, dass man einen Algorithmus verstanden hat, oder ob man eine Primitive Funktion nutzt. Deshalb ist es Quatsch `append` zu verbieten, weil man nur das falsche += kennt. Bzw. war es schon falsch, nicht von Anfang an `append` zu lernen.
Wenn man Heap-Sort nach Lehrbuch implementiert, hat man O(n + k*Log(k)).
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@divi: Ob das bei Dir im Browser in eine Zeile passt hängt auch davon ab was das CSS der Webseite macht. Das kannst Du so pauschal also gar nicht sagen.

Such Dir einen besseren Editor wenn Du da 10 Vorschläge durchscrollen musst. Eine sinnvolle Autovervollständigung sucht die Namen in denen die eingegebenen Buchstaben in der Reihenfolge *irgendwo* vorkommen, und nicht zwingend direkt hintereinander. So kann man die gleichen kryptischen Abkürzungen eintippen wie früher, als Editoren noch dumm waren, Compiler den signifikanten Teil des Namens auf 16 Zeichen beschränkten, und Dateinamen ins 8.3 Schema passen mussten.

Du kannst das mit den verschachtelten Funktionen gerne weiter behaupten. Ich glaube Dir das nicht, weil das keinen Sinn macht. Wie hilft etwas komplexer, fehleranfälliger zu machen, und sich die Möglichkeit nehmen das separat zu testen/untersuchen, dabei Fehler leichter zu finden? Das ist als wenn man sagen würde „Ich muss in diesem Haufen Sachen was finden, darum kippe ich da noch eine Kiste mit mehr Sachen drauf, damit ich das leichter finde.“

Was genau im `heapq`-Modul passiert kann man dort doch nachlesen‽
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten