Zusammenfassung
Die vorigen Lektionen der Einheit haben gelehrt, Schleifen zu schreiben und mit einer Invariante und einer Variante über sie zu argumentieren; diese Lektion wendet diese Überlegungen auf vier klassische numerische Algorithmen an, die älter sind als die Computer und noch immer im Gebrauch. Der Primzahltest durch Probedivision, der aus einem Grund, der bewiesen wird, bei aufhört und dessen Abbruchbedingung überläuft, wenn man sie nachlässig schreibt. Der euklidische Algorithmus für den größten gemeinsamen Teiler, mit seiner Invariante, seiner Terminierung, seiner Falle bei negativen Zahlen und seinem Vergleich mit std::gcd. Der Basiswechsel durch fortgesetzte Division und das Horner-Schema, das eine Zahl aus ihren Ziffern wieder aufbaut und ein Polynom nach demselben Schema auswertet. Und die Bisektion (Intervallhalbierung), die eine Nullstelle annähert, indem sie ein Intervall in zwei Hälften teilt, abgesichert durch den Zwischenwertsatz, mit einer Zahl von Durchläufen, die man im Voraus berechnet. Jeder Algorithmus wird als mathematisches Objekt vorgestellt: mit einem Beweis, dass er tut, was er verspricht, einem Beweis, dass er terminiert, und einer ersten Zählung seiner Schritte.
Lernziele
- Den Primzahltest durch Divisionen bis implementieren und begründen, warum es genügt, bis zu gehen.
- Den euklidischen Algorithmus implementieren, seine Invariante angeben (der größte gemeinsame Teiler ändert sich nicht) und ihn mit
std::gcdvergleichen. - Eine Zahl durch fortgesetzte Division in eine andere Basis umrechnen und eine Stellenwertdarstellung oder ein Polynom mit dem Horner-Schema auswerten.
- Eine Nullstelle mit der Bisektion bis auf eine Toleranz annähern und berechnen, wie viele Iterationen sie erfordert.
Teiler und Primzahlen
Probedivision
Eine ganze Zahl ist eine Primzahl, wenn ihre einzigen positiven Teiler 1 und sind; hat sie einen anderen, ist sie zusammengesetzt; 0 und 1 sind weder das eine noch das andere. Die Definition legt sofort einen Algorithmus nahe: jeden Kandidaten zwischen 2 und durchprobieren und die Zahl für zusammengesetzt erklären, sobald einer sie teilt. Das ist die Schleife des kleinsten Teilers aus der Lektion Schleifen, und sie ist korrekt. Für eine zehnstellige Primzahl führt sie jedoch fast zehn Milliarden Divisionen aus, und fast alle sind nutzlos, aus folgendem Grund.
Ist zusammengesetzt, so hat sie einen Teiler mit .
Beweis
Ist zusammengesetzt, so lässt sie sich als Produkt zweier Faktoren schreiben, von denen keiner gleich 1 oder ist. Mit wird der kleinere der beiden bezeichnet.
Multipliziert man mit der positiven Zahl , erhält man , woraus folgt: Der kleinere Faktor jeder Faktorisierung liegt links von .
Die Teiler treten also in Paaren auf, gespiegelt um : Mit sind die Paare , , und . Teilt kein Kandidat bis die Zahl , so teilt sie auch keiner darüber hinaus, denn sein Partner wäre vorher aufgetreten.
Daraus folgt, dass die Zahl der Divisionen von etwa auf etwa sinkt: für nahe zwei Milliarden von zwei Milliarden auf sechsundvierzigtausend. Das Programm verwendet das Flag-Muster aus der Lektion Korrekte Schleifen entwerfen: isPrime beginnt mit wahr, und die Schleife schaltet es aus, wenn sie einen Teiler findet.
#include <iostream>#include <print>int main(){ int n{}; std::cin >> n; bool isPrime{n >= 2}; for (int d{2}; isPrime && d <= n / d; ++d) { if (n % d == 0) { isPrime = false; } } if (isPrime) { std::println("{} ist eine Primzahl", n); } else { std::println("{} ist keine Primzahl", n); }}21474836472147483647 ist eine PrimzahlDrei Entscheidungen verdienen einen Kommentar. Das Flag wird mit n >= 2 initialisiert, sodass 0, 1 und die negativen Zahlen ohne Eintritt in die Schleife für keine Primzahl erklärt werden: Diesen Fall zu vergessen ist der typische Fehler, denn mit macht die Schleife keinen Durchlauf, und das Programm würde die 1 für eine Primzahl erklären. Die Bedingung des for hat zwei Teile: das Flag, das die Schleife beim ersten Teiler anhält, und die Schranke. Und die Schranke ist als d <= n / d geschrieben, was für positive ganze Zahlen zu äquivalent ist (die ganzzahlige Division schneidet ab, aber übersteigt den abgeschnittenen Quotienten genau dann nicht, wenn es nicht übersteigt, weil ganzzahlig ist). Die Zahl der Eingabe, , ist der größte int-Wert und eine Primzahl; das Programm entscheidet dies mit Resten, denen der Kandidaten von 2 bis .
Die Schleifeninvariante lautet: Keine ganze Zahl von 2 bis teilt , solange das Flag wahr bleibt. Sie gilt beim Eintritt, wenn das Intervall von 2 bis 1 leer ist; sie bleibt erhalten, denn wenn die Zahl nicht teilt, wächst das Intervall um eine Zahl, die sie nicht teilt; und beim Austritt mit wahrem Flag sagen die Invariante und die falsche Bedingung, , zusammen, dass kein Kandidat bis die Zahl teilt, und genau das verlangt der Satz. Die Variante ist die Anzahl der noch zu prüfenden Kandidaten, das heißt der ganzen Zahlen zwischen und : Sie sinkt um eins pro Durchlauf und fällt nicht unter null.
Die Bedingung, die überläuft
Die natürliche Art, die Schranke zu schreiben, ist d * d <= n, und sie ist ein Fehler. Mit der vorigen Eingabe erreicht die Schleife , und dann übersteigt das Produkt das Maximum von int.
#include <iostream>#include <print>int main(){ int n{}; std::cin >> n; bool isPrime{n >= 2}; for (int d{2}; isPrime && d * d <= n; ++d) { if (n % d == 0) { isPrime = false; } } std::println("Primzahl? {}", isPrime);}2147483647main.cpp:9:33: runtime error: signed integer overflow: 46341 * 46341 cannot be represented in type 'int'Das Programm endete mit dem Code 1.
Der Sanitizer für undefiniertes Verhalten hält das Programm beim Produkt an. Ohne ihn ist der vorzeichenbehaftete Überlauf undefiniertes Verhalten, und der Standard sagt nicht, was geschieht. Was man mit GCC unter Linux beobachtet, mit und ohne Optimierung, ist lehrreich: Das Produkt schlägt um und wird negativ oder kleiner als , und da kein int das Maximum übersteigt, hört die Bedingung d * d <= n nie mehr auf, erfüllt zu sein; die Schleife prüft rund zwei Milliarden Durchläufe lang Kandidaten, bis sie erreicht, das teilt, und das Programm erklärt eine Primzahl für zusammengesetzt. Ein Überlauffehler zeigt sich nicht als Überlauf, sondern als falsche Antwort. Die Form d <= n / d kann nicht überlaufen, weil sie dividiert, statt zu multiplizieren; die Lektion Bits und Mathematikbibliothek bot einen anderen Ausweg, die Schranke einmal mit std::sqrt zu berechnen, aber dann hängt die Genauigkeit der Schranke von der Gleitkommarundung ab, und die ganzzahlige Division hat dieses Problem nicht.
Die Lektion „Primzahlen und der Fundamentalsatz der Arithmetik“ aus dem Kurs Algebra und Geometrie I beweist, dass jede ganze Zahl größer als 1 auf genau eine Weise ein Produkt von Primzahlen ist; die Probedivision ist der direkteste Weg, dieses Produkt zu finden, indem man durch jeden gefundenen Teiler dividiert und mit dem Quotienten fortfährt.
Inhalt wird geladen…
Abgeschlossen
Melde dich an, um deinen Fortschritt zu speichern