C Effizient die k kleinsten Elemente aus eine Array finden

zaph42

Cadet 1st Year
Registriert
Mai 2012
Beiträge
8
Hallo,
ich stehe im Moment vor dem Problem aus einem relativ kleinem Array mit ca 100-300 Elementen die 10-30 kleinsten Elemente möglichst effizient zu suchen.
Mein Vorgehen dabei war bisher immer ein Array von Pointer auf die kleinsten Elemente des Array mit einer Art Selectionsort auszurichten. Mir geht es dabei um maximal mögliche Performance, da ich dieses Verfahren sehr oft hintereinander ausführen muss und ich derzeit Laufzeiten von mehreren Tage habe, die bisher zu ~70% vom Sortieren resultieren.
Daher wäre ich sehr dankbar wenn Ihr einen Blick auf meine Sortierfunktion werfen könntet und evtl. Schwachstellen und schlechten Programmierstil finden könntet oder gar einen besseren Algorithmus empfehlen könnt der vlt sogar in einer Library implementiert ist.
Parallelisieren ist dabei nicht erforderlich, da der Sortieralgorithmus Teil einer Rechnung ist die mit openmp mehrfach parallel ausgeführt wird.

Hier ist mal ein minimal Beispiel. Der Sortier Algorithmus ist hierbei die erste Funktion. In der Main Funktion wird die Funktion dann ein paar Mal zur Laufzeit Messung aufgerufen und anschließend der Sortier Algorithmus mit qsort aus der STL überprüft:
Code:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N  300
#define K    5

inline void __attribute__((always_inline)) psort(double* array, int n, double** pointer_array, int k)
{
 	register int i,j;
// Ersten zwei Elemente des Arrays vergleichen und Pointer initialisieren
	if( array[0] < array[1] )
	{	
		pointer_array[0] = &array[0];
		pointer_array[1] = &array[1];
	}
	else
	{
		pointer_array[0] = &array[1];
		pointer_array[1] = &array[0];
	}
// Nächsten Elemente des Arrays bis k vergleichen und Pointer damit initialisieren
	for(i=2; i != k; ++i)
	{
		if( *pointer_array[i-1] < array[i] )
		{	
			pointer_array[i] = &array[i];
		}
		else
		{	
			pointer_array[i] = pointer_array[i-1];
			j = 0;
			while( j != (i-1)  &&  *pointer_array[i-2-j] > array[i] )
			{
				pointer_array[i-1-j] = pointer_array[i-2-j];
				++j;
			}
			pointer_array[i-1-j] = &array[i];			
		}
	}
//Jetzt restlichen Elemente des Arrays mit größtem Pointer vergleichen (ggf mit nächst kleineren Pointer vergleichen) und einsortieren 
	for(i=k; i != n; ++i)
	{
		if( *pointer_array[k-1] < array[i] )
		{	
			continue;
		}
		else
		{	
			j=0;
			while(*pointer_array[k-2-j] > array[i] && j != (k-1)  )
			{
				pointer_array[k-1-j]=pointer_array[k-2-j];
				++j;
			}
			pointer_array[k-1-j]=&array[i];	
		}	
	}	
}

//Vergleichs Funktion für qsort zum Überprüfen der Richtigkeit des Sortier Algorithmus
int cmpfunc(const void * a, const void * b)
{
  if ( *(double*)a <  *(double*)b ) return -1;
  if ( *(double*)a == *(double*)b ) return 0;
  if ( *(double*)a >  *(double*)b ) return 1;
}



int main(void)
{
	//Array  definieren und mit Zufallszahlen füllen
	double array[N];
	int i;
	for(i=0; i<N; ++i) array[i]= ( rand() / (double)RAND_MAX ) * 100.0 ;			

	//Pointer array
	double *pointer_array[K];
	
	
	
	clock_t start, ende;
    	start=clock(); 		
	for(i=0; i<900000;++i)	psort(array, N, pointer_array, K);	
	ende=clock();

	printf("\n\n\tDauer in Sekunden: %f\n", (double)(ende-start)/(double)CLOCKS_PER_SEC );
	
	printf("\nKleinsten Elemente:\n");	
	for(i=0; i<K; ++i ) printf("%f \t", *pointer_array[i]);
		
	
	printf("\n\nKleinsten Elemente mit QSORT:\n");
	qsort(array, N, sizeof(double), cmpfunc);
	for(i=0; i<K; ++i )printf("%f \t", array[i]);
	
	return 0;
}
Compiliert und getestet hab ich den Code mit gcc 4.8.1 unter ubuntu.
Vielen Dank fürs Lesen und ggf. Tipps!!
 
Ohne jetzt den Code genau studiert zu haben, ist Selection Sort ein sehr schlechtes Sortierverfahren. Vielleicht solltest du mal nach Quicksort, Heaps oder Bucket Sort (wenn die zu sortierenden Werte relativ klein sind) suchen.

Gerade der Heap sollte relativ effizient sein. Du fügst alle Elemente ein und entnimmst dann 10-30 mal die Wurzel, da diese jedesmal das kleinste Element darstellt.
 
  • Gefällt mir
Reaktionen: BeBur
zaph42 schrieb:
In der Main Funktion wird die Funktion dann ein paar Mal zur Laufzeit Messung aufgerufen und anschließend der Sortier Algorithmus mit qsort aus der STL überprüft:

Erstens gibt es in C keine STL, und zweitens hat qsort() nix mit der STL zu tun. ;) Die STL ist ein Teil der Standard-Bibliothek von C++, während du hier offenbar mit C arbeitest. Wenn es dir aber wirklich um maximale Performance beim Sortieren geht, würde ich dir in diesem Fall tatsächlich den Umstieg auf C++ nahelegen, denn in C++ gibt eine std::sort()-Template-Funktion, die in der Regel wesentlich fixer ist, als das alte qsort() aus der C-Bilbiothek. Das liegt daran, daß std::sort() ein Template ist und die übergebene Vergleichsfunktion ge-inline'd werden kann, während qsort() seine Vergleichsfunktion als Pointer übergeben bekommt (da kann selten was ge'inline'd werden).

The fact that function pointer parameters inhibit inlining explains an observation that long-time C programmers often find hard to believe: C++’s sort virtually always embarrasses C’s qsort when it comes to speed. Sure, C++ has function and class templates to instantiate and funny-looking operator() functions to invoke while C makes a simple function call, but all that C++ “overhead” is absorbed during compilation. At runtime, sort makes inline calls to its comparison function (assuming the comparison function has been declared inline and its body is available during compilation) while qsort calls its comparison function through a pointer. The end result is that sort runs faster. In my tests on a vector of a million doubles, it ran up to 670% faster, but don’t take my word for it, try it yourself. It’s easy to verify that when comparing function objects and real functions as algorithm parameters, there’s an abstraction bonus.

http://stackoverflow.com/questions/16369807/the-benefit-of-function-objects
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: BeBur
Falls C++ eine Option darstellt: Je nachdem was dein genauer Anwendungsfall ist und wie deine Verteilung aussieht könntest du auch noch einen Blick auf die Funktion hier werfen und schauen ob sie verwertbar ist, da die Komplexität bei O(n) liegt und nicht bei O(n*log(n)) wie bei allem was komplett durchsortiert werden muss:

http://www.cplusplus.com/reference/algorithm/nth_element/

Falls C++ keine Option ist lässt sich sowas ähnliches vielleicht in C finden/nachbauen.
 
Wenn ich deinen Sortieralgorithmus richtig verstanden habe, müsste er eine Best-Case Laufzeit in O(n) haben. Diese aber nur im Falle eines richtig sortierten Arrays. Bei einem unsortierten Array wird die while-Schleife in der for-Schleife mit Sicherheit mehrfach durchlaufen (maximal K mal). Damit multiplizierst du einen konstanten Faktor auf deine Laufzeit O(k*n). Diesen ignoriert man zwar bei der Laufzeitbetrachtung, aber er ist dennoch da.

Heapsort hat eine Laufzeit in O(n log(n)). Diese wird dein Algorithmus wahrscheinlich nicht erreichen. Ich würde an deiner Stelle Quick- oder Heapsort einsetzen.
 
Kleiner Hinweis noch:
Wenn der Unterschied zwischen: Gesamtmengengröße=n und k (von k-kleinste) groß genug ist, kannst du auch einfach k mal das Minimum suchen und dann das Minimum entfernen.. hat O(k * n) Laufzeit...

Bei 300 vs. 10 sicherlich effizienter als O(n * log(n)), bei 300 vs 200 sicherlich nicht...
 
Verzeihung, Lens hat natürlich Recht *g*, selbst wenn man mit dem natürlichen Logarithmus rechnet wie ich, lohnt es sich bereits ab 6 durchläufen ;) - Laufzeit wird aber auch net mit log_n berechnet...

*shame*..
 
@kinglouy: Muss mir nochmal Quicksort und Heapsort anschaun ob es dort gute möglichkeiten gibt aus kleinen arrays die kleinsten Elemente herauszuchen. Experimente mit Quicksort waren langsamer, da ich ja nur sehr kleine Array sortiere und meine Implementation von Quicksort seine stärke erst bei größeren Arrays ausgespielt hat.

@antred: Sorry! Natürlich ist qsort aus der C standard library. Aber qsort verwende ich ja nur in diesem Beispiel um die Richtigkeit der selber geschriebenen Funktion zu überprüfen. In meinem richtigen Programm kommt diese nicht vor.
C++ wollte ich vermeiden, außerdem sortiert sort() ja das gesamt array bzw. vector. Eher würde die Funktion partial_sort() zutreffen. Aber eine frühere C++ Version meines Programms hatte diese verwendet und hatte dabei eine schlechtere Performance als mit dem selber geschrieben Pointer sortieren. Wobei ich mein Programm in C komplett neu geschrieben hab und dabei auch andere Performanceprobleme optimiert habe.


@Cordless: Danke aber wie eben erwähnt C++ wollte ich eher vermeiden, aber ich schaus mir nochmal an.

@andere: Vielen Dank Quick- und Heapsort geh ich gerade nochmal durch. Meine Anwendung im Programm ist wirklich sehr ähnlich wie im obigen Beispiel:
ein 300 Elemente großes Array wird durch eine Monte Carlo Simulation gefüllt und anschließend brauch ich die 10-30 kleinsten Werte und davon wirklich alle, da daraus wieder eine Art Durchschnitt berechnet wird. Aber es werden nie mehr kleine Elemente benötigt als 10% der Arraygröße
 
Ich denke, dass dein Problem weniger mit Sortieren, als mit der Medianberechnung verwandt ist. Für k-größte (kleinst) Element gibt es O(n) Algorithmen, basierend auf Divide-And-Conquer. Algorithmus siehe hier.
Ob das allerdings bei deinen kleinen N und K schneller ist, ist eine andere Frage...
 
  • Gefällt mir
Reaktionen: BeBur
Mal fix mit valgrind ne cache- und branch-simulation gemacht, keine Cacheprobleme und die Branchprediction liegt meistens richtig.

Wenn die Workload wirklich nur so klein ist, dürfte das nah am Optimum sein. Ich wäre mir nicht sicher, ob ein anderer Algo deutlich schneller ist, wenn eh alles in den Cache passt.

Ich nehme mal an, du kompilierst eh mit -O3. Ist -ffast-math eine Option? Evtl auch noch -mtune=native -march=native – kommt drauf an, wofür du es brauchst.
 
Tust du mir einen Gefallen und benchmarkst einmal folgende Variante?

Code:
// Die K kleinsten stehen am ENDE.
void partial_bubble_sort(int array[], int N, int K)
{
  for (int i = 0; i < K; i+=1)
    for (int j = 0; j < N - i - 1; j+=1)
      if (array[j] < array[j+1])
        {
          int temp = array[j];
          array[j] = array[j+1];
          array[j+1] = temp;
        }
}
 
Zuletzt bearbeitet:
Blabla Bloblo schrieb:
Kleiner Hinweis noch:
Wenn der Unterschied zwischen: Gesamtmengengröße=n und k (von k-kleinste) groß genug ist, kannst du auch einfach k mal das Minimum suchen und dann das Minimum entfernen.. hat O(k * n) Laufzeit...

Bei 300 vs. 10 sicherlich effizienter als O(n * log(n)), bei 300 vs 200 sicherlich nicht...

wenn ich mir schon zu so etwas gedanken mache, dann richtig:

Vairante 1:
einfacher, aber langsamer (immer noch viel schneller als dein vorschlag oben - nicht als vorwurf gemeint!).

result : eine anfänglich leere liste, die sortiert gehalten wird (beim einfügen eines neuen elements wird seine position bestimmt und eingefügt) oder ein max-heap.

eingabe ist ein array input mit N-zahlen. gesucht sind die K-kleinesten.

man geht das array input nur einmal durch:
füge das betrachte element aus input in result ein.

ist |result| > K dann lösche das größte element aus result.

laufzeit O(n*k) mit der sortierten liste (hat gleiche laufzeit wie der zitierte vorschlag) und O(n*log(k)) mit der heap variante.


beste lösung ist aber variante 2:

wi bei quicksort wird partitioniert, wobei jedoch in jedem schritt eine der beiden partitionen "vergessen" wird.

Code:
while( |result|<k ) {
     wähle zufälliges 0<=p<=n;

     partitioniere alle elemente des arrays input gemäß p:
            ergebniss sind zwei partitionen: part_links und part_rechts

    falls |part_links| >= k {
         input = part_links
    } sonst {
         füge zu result alles aus part_links hinzu
         k = k-|part_links|
         input = part_rechts
    }
}

laufzeit ist Theta( n ) im erwartungsfall (p ist zufällig gewählt). damit es das auch im worstcase fall hält muss das p als median bestimmt werden. das ist aber etwas komplizierter und lohnt sich aber in der praxis selten.


NACHTRAG: variante 2 ist übrigens gut geeignet auf systemen mit guter array unterstützung, sofern man es richtig implementiert.
das gilt für vairante 1 mehr oder weniger nur, wenn man einen max-heap verwendet, der als array abgebildet wird (binärer heap).
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: KitKat::new()
Falls mal jemand nach einer anderen möglichen Variante dieses Problems sucht:

1. Suche mit Quickselect (random pivot) das k kleinste Element = x (Average O(n)),
2. laufe über das Array und merke dir alle kleineren Elemente als x (in O(n))).

Worst case O(n^2), average O(n).
 
  • Gefällt mir
Reaktionen: blöderidiot und BeBur
@Oscar While - thats the way to go!
Das ist zwar ein Thread aus dem Mittelalter, aber egal!

Hier eine Variante (300 Elemente, 900000X):
Code:
Kleinste Elemente:
0.125130        0.161753        0.466947        0.878960        0.891168
        Dauer in Sekunden: 0.655000
C:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N  300
#define K    5

 void pminextract(double* array, int n, int k)
{
 int i, j, minindex, nrun = n;
 for (j = 0; j < k; j++) {
    // find the index of the minimum value
    for(minindex = 0, i = 1; i < nrun; i++) {
       if (array[minindex] > array[i])
          minindex = i;
    }
    // swap item to the back of the array
    double tmp = array[nrun - 1];
    array[nrun - 1] = array[minindex];
    array[minindex] = tmp;
    // and reduce array length
    nrun--;
 }
}

 int main(void)
{

 int i;
 double array[N]; // Array  definieren und mit Zufallszahlen füllen
 for (i = 0; i < N; ++i)
    array[i] = (rand() / (double)(RAND_MAX - 1)) * 100.0;

 // Pointer array
 double* pointer_array[K];

 clock_t start, ende;
 start = clock();
 for (i = 0; i < 900000; ++i) {
    pminextract(array, N, K);
 }
 ende = clock();
 
 printf("\nKleinste Elemente:\n");
 for (i = 0; i < K; ++i) {
    pointer_array[i] = &array[N - i - 1];
    printf("%f \t", *pointer_array[i]);
 }

 printf("\n\n\tDauer in Sekunden: %f\n",
        (double)(ende - start) / (double)CLOCKS_PER_SEC);

 return 0;
}
Also kein Sortierproblem, sondern ein Bereichs-Suchproblem. Wobei bei solch kleinen Vektoren hier eine einfache lineare Suche auf konsekutivem Speicher effizienter sein sollte.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: KitKat::new() und BAGZZlash
@blöderidiot hab ich grade einen Denkfehler oder ist dein Array nach dem ersten Durchlauf für die verbleibenden 899999 schon korrekt? Sollte da nicht vor jedem Durchlauf das Array neu geneiert werden?
 
@Ebrithil , der TE erstellte einmal das array und machte dann 900,000 runs über das array um eine Art Benchmark zu haben. Warum er das so macht weiß ich nicht aber ich habe das halt so nachgebildet.

Ein cleverer Compiler könnte allerdings erkennen, dass ich nur die Werte aus dem letzten Durchlauf weiterverwende ...
 
zaph42 schrieb:
Compiliert und getestet hab ich den Code mit gcc 4.8.1 unter ubuntu.
Vielen Dank fürs Lesen und ggf. Tipps!!
Glaube es hat noch niemand gesagt.
Wechsel mal auf eine zeitgemäße Version, mein Ubuntu kommt mit 9.3 oder etwas um den dreh.
Das alleine könnte schon was bringen.

Edit. Argh, immer diese Leichenschändung.
 
Zurück
Oben