Ich suche nach dem schnellsten Weg, um festzustellen, ob ein long
-Wert eine Quadratzahl ist (d. h., ob seine Quadratwurzel eine andere ganze Zahl ist):
- Ich habe es auf einfache Weise gemacht, indem ich die integrierte
Math.sqrt()
Funktion verwendet habe, aber ich frage mich, ob es einen schnelleren Weg gibt, indem man sich auf den Bereich der Ganzzahlen beschränkt. - Das Führen einer Lookup-Tabelle ist unpraktisch (da es etwa 231,5 Ganzzahlen gibt, deren Quadrat kleiner als 263 ist).
Hier ist der sehr einfache und direkte Weg, den ich jetzt gehe:
public final static boolean isPerfectSquare(long n)
{
if (n < 0)
return false;
long tst = (long)(Math.sqrt(n) + 0.5);
return tst*tst == n;
}
Hinweis: Ich verwende diese Funktion in vielen <a href="http://projecteuler.net/" rel="noreferrer">Project Euler</a> Problemen. Also wird niemand anderer jemals diesen Code pflegen müssen. Und diese Art von Mikro-Optimierung könnte tatsächlich einen Unterschied machen, da ein Teil der Herausforderung darin besteht, jeden Algorithmus in weniger als einer Minute auszuführen, und diese Funktion in einigen Problemen Millionen von Malen aufgerufen werden muss.
Ich habe die verschiedenen Lösungen für das Problem ausprobiert:
- Nach umfangreichen Tests habe ich festgestellt, dass das Hinzufügen von
0.5
zum Ergebnis von Math.sqrt() nicht notwendig ist, zumindest nicht auf meinem Computer. - Der schnelle Kehrwert der Quadratwurzel war schneller, gab aber falsche Ergebnisse für n >= 410881. Jedoch, wie von BobbyShaftoe vorgeschlagen, können wir den FISR-Hack für n < 410881 verwenden.
- Newton's Methode war deutlich langsamer als
Math.sqrt()
. Das liegt wahrscheinlich daran, dassMath.sqrt()
etwas Ähnliches wie Newton's Methode verwendet, aber in der Hardware implementiert ist, so dass sie viel schneller als in Java ist. Außerdem erforderte Newton's Methode immer noch die Verwendung von Doubles. - Eine modifizierte Newton-Methode, die ein paar Tricks verwendete, so dass nur Ganzzahlenmathematik beteiligt war, erforderte einige Hacks, um Überlauf zu vermeiden (Ich möchte, dass diese Funktion mit allen positiven 64-Bit-Ganzzahlen funktioniert), und war immer noch langsamer als
Math.sqrt()
. - Die binäre Suche war noch langsamer. Das ergibt Sinn, da die binäre Suche im Durchschnitt 16 Durchläufe benötigen wird, um die Quadratwurzel einer 64-Bit-Zahl zu finden.
- Laut Johns Tests ist die Verwendung von
or
-Anweisungen in C++ schneller als die Verwendung einesswitch
, aber in Java und C# scheint es keinen Unterschied zwischenor
undswitch
zu geben. - Ich habe auch versucht, eine Lookup-Tabelle zu erstellen (als ein privates statisches Array von 64 booleschen Werten). Dann anstatt eines Switch- oder
or
-Statements würde ich einfach sagenif(lookup[(int)(n&0x3F)]) { test } else return false;
. Zu meiner Überraschung war dies (nur leicht) langsamer. Das liegt daran, dass in Java Arraygrenzen überprüft werden.
0 Stimmen
Da Integer und Long in den meisten c-ähnlichen Sprachen keine spezifische Länge haben (was Ihr Code zu sein scheint), ist es besser zu sagen, dass es für ein 32-Bit-Integer 2**16 perfekte Quadrate gibt.
28 Stimmen
Dies ist Java-Code, bei dem int==32 Bits und long==64 Bits sind, und beide sind vorzeichenbehaftet.
0 Stimmen
Welches ist schneller: "long tst = (long)Math.sqrt(n); return tst*tst == n;" (was du hast) oder "double tst = Math.sqrt(n); return tst ==(double)Math.round(tst);" ?
0 Stimmen
Welche JVM haben Sie für das Testen verwendet? In meiner Erfahrung hängt die Leistung des Algorithmus von der JVM ab.
16 Stimmen
@Shreevasta: Ich habe einige Tests mit großen Werten (größer als 2^53) durchgeführt und deine Methode liefert einige falsche positive Ergebnisse. Das erste, das auftritt, ist für n=9007199326062755, das keine perfekte Quadratzahl ist, aber als solche zurückgegeben wird.
0 Stimmen
Es gibt einen Fehler in Ihrem Code: sqrt = (long)Math.sqrt(n); return sqrt*sqrt == n; Math.sqrt(n) gibt eine Gleitkommazahlenrepräsentation, z.B. Math.sqrt(9) könnte 2,99999999999 zurückgeben, wenn Sie Pech haben, und wenn Sie es mit (long) casten, könnten Sie leicht eine zu niedrige Zahl haben.
0 Stimmen
Sie können Bittricks verwenden, um die letzten 6 Bits zu überprüfen: if( (x&2) || ((x & 7) == 5) || ((x & 11) == 8) || ((x & 31) == 20) || ((x & 47) == 24)) return false;
39 Stimmen
Bitte nennen Sie es nicht den "John Carmack Hack". Er hat es nicht erfunden.
1 Stimmen
@martinus: Für Werte unter 2^53 ist die doppelte Darstellung genau, sodass es keinen Rundungsfehler gibt. Ich habe auch bei jedem vollkommenen Quadrat größer als 2^53 getestet, sowie +/- 1 von jedem vollkommenen Quadrat, und Rundungsfehler führen nie zu einer falschen Antwort.
0 Stimmen
Ich glaube, dass moderne JVMs möglicherweise in der Lage sind, Array-Indexprüfungen an einer bestimmten Stelle zu überspringen, wenn daraus geschlossen werden kann, dass sie dort immer gültig sein werden. Mit welcher JVM wurde der Test durchgeführt?
0 Stimmen
Der "John Carmack-Hack" sollte für einen größeren Bereich möglich sein, indem die zusätzliche Durchlaufdurchführung im Quellcode von Quake auskommentiert wird, wenn nötig (d.h. eine große genug Zahl).
0 Stimmen
@Thorbjørn Ravn Andersen: Ich habe den J2SE 6.0 Windows JVM verwendet, den ich von der Website von Sun heruntergeladen habe. Ich habe auch versucht, die zusätzliche Iteration zu kommentieren, und wenn ich mich richtig erinnere, wurde es auf seltsame Weise weniger genau.
0 Stimmen
Machen Sie Ihren Quickfail schneller und machen // Quickfail if( n < 0 || ((n&2) != 0) || ((n & 7) == 5) || ((n & 11) == 8) ) return false; if( n == 0 ) return true; andersherum: // Quickfail if( n == 0 ) return true; if( n < 0 || ((n&2) != 0) || ((n & 7) == 5) || ((n & 11) == 8) ) return false;
0 Stimmen
@dstibbe: Das würde nur schneller sein, wenn die Eingabe 0 ist. Für 75% der anderen Eingaben (ohne negative Zahlen zu zählen) wird es schneller sein, wie es geschrieben ist, und für die anderen 25% wird es keinen Unterschied geben.
98 Stimmen
@mamama - Vielleicht, aber ihm wird das zugeschrieben. Henry Ford erfand nicht das Auto, die Wright Brüder erfanden nicht das Flugzeug, und Galileo war nicht der erste, der herausfand, dass sich die Erde um die Sonne drehte... Die Welt besteht aus gestohlenen Erfindungen (und Liebe).
0 Stimmen
@Kip, der Mikrobenchmark für den Algorithmus könnte wegen der relativ großen Tabelle ungenau sein. Wenn die gesamte Tabelle im Cache ist (enge Schleifen), wird es keine Cache-Misses geben. Das boolean[] sollte durch lange Konstanten (oder ein Array) ersetzt werden und wird dadurch etwas schneller.
4 Stimmen
Sie könnten eine geringfügige Geschwindigkeitssteigerung im 'quickfail' erzielen, wenn Sie etwas wie
((1<<(n&15))|65004) != 0
verwenden, anstatt drei separate Überprüfungen zu haben.1 Stimmen
Warum fügst du das 0.5 hinzu?
1 Stimmen
@KorayTugay, um die Zahl zu runden. Mein Bedenken war, dass Math.sqrt einen Wert zurückgeben könnte, der aufgrund von Rundungsfehlern leicht abweicht. Angenommen, wenn
math.sqrt(100)
9.999999999999999
anstelle von10
zurückgeben würde. Ich bin mir jedoch nicht sicher, ob es tatsächlich Fälle gibt, in denen dies passiert.0 Stimmen
@Kip schau dir meine Antwort an, ich habe einen signifikanten Leistungsgewinn auf deine gepostete Antwort erzielt.
0 Stimmen
@Kip Ich habe meinen Beitrag bearbeitet, um einen dritten Algorithmus hinzuzufügen, der manchmal besser abschneidet als meine vorherige Antwort.
1 Stimmen
Überraschenderweise habe ich auf eine so beliebte Frage niemanden hingewiesen gesehen, dass man bei höherem
n
genauer sein kann, indem man eine weitere Iteration durchführt, die als 'Carmack-Hack' bezeichnet wird. Hinweis: Das Ergebnis stammt nicht von Carmack, sondern von Newton/Raphson. Ich vermute, dass der 'Magische Zahl'-Hack eine fairere Zuschreibung an Carmack wäre.1 Stimmen
Nicht sicher, ob dies das ist, was du willst, aber das dauert etwa 1 Millisekunde. (JavaScript, nicht Java.)
Rückgabe math.sqrt(x).split(".").length > 1
0 Stimmen
Ich bin spät zur Party gekommen, aber ich glaube, du könntest noch mehr Leistung aus deinem endgültigen Code-Schnipsel herausholen, indem du Techniken von hier verwendest: stackoverflow.com/questions/15621083/…
1 Stimmen
@user3932000 Ich habe das gemacht, weil es zu einer leichten Compiler-Optimierung führen kann. Hier ist eine bessere Diskussion: stackoverflow.com/questions/5547663/…
1 Stimmen
Ich bin noch nie auf ein PE-Problem gestoßen, das nicht in unter einer Minute in Ruby berechnet werden könnte. Daher ist das Argument, dass die Leistung für dieses Ergebnis wichtig ist, ein wenig unglaublich.
0 Stimmen
Ich bin gespannt, wie sich diese Weisheit angesichts von Innovationen wie SSE SQRTPS bewährt. felixcloutier.com/x86/SQRTPS.html,
0 Stimmen
Was ist ein Beispiel für ein Problem, bei dem das einen Unterschied macht? Anstatt einzelne Quadratwurzeln für eine lange Liste zu berechnen, ist es viel besser, die Liste der perfekten Quadrate abzuarbeiten und die Liste auf diese Weise zu filtern.
1 Stimmen
Für sehr große Zahlen gibt es einen schnellen randomisierten Algorithmus: petr-mitrichev.blogspot.com/2017/12/a-quadratic-week.html
2 Stimmen
@RobertFraser Galileo Galilei war nicht der Erste, dessen Name verstümmelt wurde.
1 Stimmen
@RobertFraser: Ja, und das sind große Ungerechtigkeiten, gegen die du nichts unternehmen kannst. Dies ist niemals eine Rechtfertigung für dein schlechtes Verhalten.
0 Stimmen
Ich würde vorschlagen, einen Integer-Quadratwurzelalgorithmus zu verwenden stackoverflow.com/questions/1100090
2 Stimmen
@RobertFraser Ich habe noch nie gehört, dass jemand behauptet hat, dass Galileo das heliozentrische Modell erfunden hat. Es wird immer Kopernikus zugeschrieben. Meintest du ihn?