Zwei Zeiger
Kurz gesagt: Zwei Zeiger, die an beiden Enden eines sortierten Arrays beginnen und sich aufeinander zubewegen, finden ein Paar mit gegebener Summe in O(n) statt O(n²).
Problem
Die Geschenkkarte und jedes Paar
Ein Kunde hat eine Geschenkkarte und will sie auf den Cent ausgeben: zwei Produkte, die zusammen genau so viel kosten wie die Karte. Die Seite sucht sie mit zwei verschachtelten Schleifen und prüft jedes Paar:
let checks = 0;
function allPairs(prices, card) { for (let i = 0; i < prices.length; i++) { for (let j = i + 1; j < prices.length; j++) { checks++; if (prices[i] + prices[j] === card) { return [i, j]; } } } return null;}Die Preisliste sind 1000 Produkte zu 2 €, 4 €, …, 2000 €, und die Karte hat 2001 €. Die Summe zweier gerader Preise ist nie ungerade, also gibt es kein Paar. Das ist der schlimmste Fall: Die Schleifen laufen bis zum Ende.
const n = 1000;const prices = Array.from({ length: n }, (_, i) => 2 * (i + 1));const card = 2 * n + 1;const pair = allPairs(prices, card);console.log("Paar: " + (pair === null ? "keins" : "gefunden"));console.log(`Prüfungen: ${checks}`);Paar: keinsPrüfungen: 499500Fast eine halbe Million Prüfungen für 1000 Produkte. Es gibt n(n − 1) / 2 Paare, also ist das O(n²): Bei 100.000 Produkten werden es etwa 5 Milliarden.
Und die Preisliste ist nach Preis sortiert, doch die Schleifen nutzen das nicht.
Die Idee
Einer links, der andere rechts
Leg einen Finger auf den günstigsten Preis und einen anderen auf den teuersten. Die Finger sind Indizes: lo und hi. Addiere die beiden Preise:
- ist die Summe gleich der Karte, bist du fertig;
- ist sie zu klein, scheidet der günstige Preis aus: Selbst mit dem teuersten Preis im Spiel reicht es nicht.
logeht einen Preis nach rechts; - ist sie zu groß, scheidet der teure Preis aus: Er ist selbst mit dem günstigsten zu viel.
higeht einen Preis nach links.
Jeder Schritt wirft einen Preis endgültig weg, und kein Paar, das die Antwort ist, geht verloren. Treffen sich lo und hi, sind die Preise aufgebraucht. Höchstens n − 1 Schritte: O(n).
Das heißt zwei Zeiger: zwei Indizes, die am Array entlanggehen, und jeder Vergleich sagt, welcher von ihnen sich bewegt. Hier gehen sie aufeinander zu. Die Idee kennst du aus der binären Suche, wo lo und hi den Bereich halten. Dort wirft jede Probe die Hälfte weg, hier einen Preis.
Die Bedingung ist dieselbe: Das Array muss sortiert sein. Nur dann stimmt „weiter rechts ist teurer“.
Sieh es dir an
lo und hi in der Preisliste
function findPair(prices, card) { let lo = 0; let hi = prices.length - 1; while (lo < hi) { const sum = prices[lo] + prices[hi]; if (sum === card) { return [lo, hi]; } else if (sum < card) { lo++; } else { hi--; } } return null;}const prices = [3, 5, 8, 12, 15, 19, 24, 27, 31, 36];const pair = findPair(prices, 50);if (pair === null) { console.log("Kein Paar für 50 €");} else { const [lo, hi] = pair; console.log(`${prices[lo]} € + ${prices[hi]} €`);}19 € + 31 €function findPair(prices, card) { let lo = 0; let hi = prices.length - 1; while (lo < hi) { const sum = prices[lo] + prices[hi]; if (sum === card) { return [lo, hi]; } else if (sum < card) { lo++; } else { hi--; } } return null;}Schritt für Schritt, in Worten
- Die Preisliste ist sortiert: 10 Preise. Die Geschenkkarte hat 50 €. lo steht auf dem günstigsten Preis, hi auf dem teuersten.
- 3 € + 36 € = 39 € < 50 €: zu wenig. Selbst der teuerste Preis reicht für 3 € nicht, also scheidet 3 € aus: lo geht nach rechts.
- 5 € + 36 € = 41 € < 50 €: zu wenig. Selbst der teuerste Preis reicht für 5 € nicht, also scheidet 5 € aus: lo geht nach rechts.
- 8 € + 36 € = 44 € < 50 €: zu wenig. Selbst der teuerste Preis reicht für 8 € nicht, also scheidet 8 € aus: lo geht nach rechts.
- 12 € + 36 € = 48 € < 50 €: zu wenig. Selbst der teuerste Preis reicht für 12 € nicht, also scheidet 12 € aus: lo geht nach rechts.
- 15 € + 36 € = 51 € > 50 €: zu viel. 36 € ist selbst mit dem günstigsten Preis zu viel, also scheidet 36 € aus: hi geht nach links.
- 15 € + 31 € = 46 € < 50 €: zu wenig. Selbst der teuerste Preis reicht für 15 € nicht, also scheidet 15 € aus: lo geht nach rechts.
- 19 € + 31 € = 50 €: genau die Karte. findPair gibt die Indizes 5 und 8 zurück.
- 7 Summen statt 45 Paare. Jeder Schritt wirft einen Preis endgültig weg, also sind es höchstens n − 1 Schritte.
null, wenn es kein Paar gibt. const [lo, hi] = pair zerlegt sie.Genauer, für Neugierige
Warum kein Paar verloren geht. Sagen wir, die Antwort ist das Indexpaar i und j, i < j. Solange lo ≤ i und hi ≥ j, können die Zeiger die Antwort nicht überspringen:
- ist
lo< i, ist die Summe ausloundhihöchstens die Summe aus i undhi. Ist sie zu klein, gehtlozu Recht nach rechts, ohne i zu überschreiten; - ist
hi> j, geht auf dieselbe Weisehizu Recht nach links, ohne j zu überschreiten.
Erreicht lo das i und hi das j, ist die Summe genau die Karte. Gibt es mehrere Paare, finden die Zeiger das äußerste: das mit dem günstigsten Preis.
Probier es aus
Sag voraus, welches Paar
Die Karte hat 43 €. Die Preisliste hat zwei Paare mit dieser Summe: 12 € + 31 € und 19 € + 24 €. Was gibt das Programm aus?
function findPair(prices, card) { let lo = 0; let hi = prices.length - 1; while (lo < hi) { const sum = prices[lo] + prices[hi]; if (sum === card) { return [lo, hi]; } else if (sum < card) { lo++; } else { hi--; } } return null;}
const prices = [3, 5, 8, 12, 15, 19, 24, 27, 31, 36];const pair = findPair(prices, 43);if (pair === null) { console.log("Kein Paar für 43 €");} else { const [lo, hi] = pair; console.log(`${prices[lo]} € + ${prices[hi]} €`);}39 € sind zu wenig: lo geht zu 5 €. 41 € sind zu wenig: lo geht zu 8 €. 44 € sind zu viel: hi geht zu 31 €. 8 € + 31 € = 39 € sind zu wenig: lo geht zu 12 €. 12 € + 31 € = 43 €, fertig. Die Zeiger kommen von außen nach innen, also finden sie das äußere Paar, und 19 € + 24 € liegt innerhalb davon.
Probier es aus
Ergänze
Die Summe ist kleiner als die Karte. Welcher Zeiger bewegt sich, und in welche Richtung?
function findPair(prices, card) {
let lo = 0;
let hi = prices.length - 1;
while (lo < hi) {
const sum = prices[lo] + prices[hi];
if (sum === card) {
return [lo, hi];
} else if (sum < card) {
;
} else {
hi--;
}
}
return null;
}Die Summe muss wachsen, und der einzige Weg ist ein teurerer Preis links: lo geht nach rechts. hi nach links würde die Summe noch kleiner machen.
Probier es aus
Finde den Fehler
Die Karte hat 50 €, und 19 € + 31 € stehen in der Preisliste. Die Funktion sagt, es gibt kein Paar:
const prices = [3, 5, 8, 12, 15, 19, 24, 27, 31, 36];const pair = findPair(prices, 50);if (pair === null) { console.log("Kein Paar für 50 €");} else { const [lo, hi] = pair; console.log(`${prices[lo]} € + ${prices[hi]} €`);}Kein Paar für 50 €Wähle die Zeile mit dem Fehler.
Ist die Summe zu klein, bewegt der Code hi nach links. So geht die Summe nur nach unten: 3 € + 36 €, dann 3 € + 31 €, 3 € + 27 € und so weiter, bis hi bei lo ankommt. lo bewegt sich gar nicht. Die Lösung ist lo nach rechts, und die Funktion gibt 19 € + 31 € zurück.
Sieh es dir an
Wie viele Schritte
Dieselbe Preisliste mit 1000 Produkten und die Karte über 2001 €, und die Funktion zählt die Schritte ihrer Schleife:
let steps = 0;
function findPair(prices, card) { let lo = 0; let hi = prices.length - 1; while (lo < hi) { steps++; const sum = prices[lo] + prices[hi]; if (sum === card) { return [lo, hi]; } else if (sum < card) { lo++; } else { hi--; } } return null;}const n = 1000;const prices = Array.from({ length: n }, (_, i) => 2 * (i + 1));const card = 2 * n + 1;const pair = findPair(prices, card);console.log("Paar: " + (pair === null ? "keins" : "gefunden"));console.log(`Schritte: ${steps}`);Paar: keinsSchritte: 999999 Schritte statt 499.500 Prüfungen. Die zwei Funktionen geben dieselbe Antwort, und bei 100.000 Produkten ist der Unterschied 99.999 Schritte gegen etwa 5 Milliarden.
Lass sie im Browser laufen. Die dritte Kurve gilt für eine unsortierte Preisliste: erst Mergesort, dann die zwei Zeiger.
- Jedes Paar
O(n²) - Zwei Zeiger (sortierte Liste)
O(n) - Sortieren, dann zwei Zeiger
O(n log n)
| Algorithmus | n = 10 | n = 1.000 | n = 10.000 | n = 100.000 |
|---|---|---|---|---|
| Jedes Paar | 45 | 499.500 | 49.995.000 | ≈ 4.999.500.000 |
| Zwei Zeiger (sortierte Liste) | 9 | 999 | 9.999 | 99.999 |
| Sortieren, dann zwei Zeiger | 31 | 9.692 | 130.424 | 1.636.539 |
function findPair(prices, card) { let lo = 0; let hi = prices.length - 1; while (lo < hi) { const sum = prices[lo] + prices[hi]; if (sum === card) { return [lo, hi]; } else if (sum < card) { lo++; } else { hi--; } } return null;}Über 10.000 lassen wir nicht jedes Paar laufen: Diese Werte sind mit ≈ gekennzeichnet. Das Sortieren kostet mehr als die Zeiger selbst, aber seine Kurve ist O(n log n) und liegt immer noch weit unter O(n²).
Die Idee
Ist die Liste sortiert
Zwei Zeiger brauchen ein sortiertes Array. Ist es das nicht:
- Sortiere es zuerst. Sortieren ist O(n log n), die Zeiger O(n), insgesamt O(n log n). Bei 100.000 Preisen sind das etwa 1,6 Millionen Operationen gegen etwa 5 Milliarden. Sortieren gewinnt fast immer.
- Wenn du oft suchst in derselben Liste (jeder Kunde mit einer anderen Karte), sortierst du einmal, und jede Suche danach ist O(n).
- Die Indizes ändern sich. Nach dem Sortieren ist
prices[5]nicht mehr das fünfte Produkt des Katalogs. Brauchst du die Produkte, sortiere Paare (Preis, Produkt) oder Objekte nach Preis.
const prices = [27, 3, 36, 15, 8, 31, 19, 5, 24, 12];prices.sort((a, b) => a - b);console.log("[" + prices.join(", ") + "]");const pair = findPair(prices, 50);if (pair === null) { console.log("Kein Paar für 50 €");} else { const [lo, hi] = pair; console.log(`${prices[lo]} € + ${prices[hi]} €`);}[3, 5, 8, 12, 15, 19, 24, 27, 31, 36]19 € + 31 €sort() in JavaScript ohne Vergleichsfunktion sortiert die Zahlen als Text: „12“ kommt vor „3“, weil „1“ vor „3“ kommt. Für Zahlen gibst du immer (a, b) => a - b an. Ohne sie beginnen die zwei Zeiger mit der falschen Reihenfolge und verpassen Paare:
const prices = [27, 3, 36, 15, 8, 31, 19, 5, 24, 12];prices.sort();console.log("[" + prices.join(", ") + "]");[12, 15, 19, 24, 27, 3, 31, 36, 5, 8]Es gibt auch einen Weg ohne Sortieren: Du gehst einmal über die Preise und behältst die gesehenen in einem Set. Für jeden Preis fragst du, ob card − price schon gesehen wurde. Das ist im Durchschnitt O(n), mit einer Hashtabelle und n zusätzlichem Speicher. Zwei Zeiger brauchen keinen zusätzlichen Speicher, aber ein sortiertes Array.
Sieh es dir an
Dieselbe Idee: Mischen und Duplikate
Die zwei Zeiger gehen nicht immer aufeinander zu. Manchmal gehen sie in dieselbe Richtung.
Zwei sortierte Listen mischen. Die Bestellungen von der Website und vom Telefon werden von einem Zähler nummeriert, aber getrennt geführt, jede Liste sortiert. Der Tagesbericht will eine sortierte Liste. i geht an der ersten entlang, j an der zweiten, und jedes Mal kommt die kleinere Nummer ins Ergebnis. Das ist das Mischen aus dem Mergesort:
function merge(a, b) { const result = []; let i = 0; let j = 0; while (i < a.length && j < b.length) { if (a[i] <= b[j]) { result.push(a[i++]); } else { result.push(b[j++]); } } while (i < a.length) { result.push(a[i++]); } while (j < b.length) { result.push(b[j++]); } return result;}const web = [1003, 1007, 1012, 1020];const phone = [1001, 1009, 1010, 1031, 1044];console.log("[" + merge(web, phone).join(", ") + "]");[1001, 1003, 1007, 1009, 1010, 1012, 1020, 1031, 1044]Duplikate an Ort und Stelle. Die Preise aus einem Import sind sortiert, aber mit Duplikaten, und der Filter muss jeden Preis einmal zeigen. read liest jeden Preis, und write ist der Platz für den nächsten neuen. Alles vor write ist fertig und frei von Duplikaten:
function unique(prices) { if (prices.length === 0) { return 0; } let write = 1; for (let read = 1; read < prices.length; read++) { if (prices[read] !== prices[write - 1]) { prices[write] = prices[read]; write++; } } return write;}const prices = [5, 5, 8, 12, 12, 12, 19, 24, 24];const k = unique(prices);prices.length = k;console.log(`Verschiedene Preise: ${k}`);console.log("[" + prices.join(", ") + "]");Verschiedene Preise: 5[5, 8, 12, 19, 24]prices.length = k entfernt alles nach den ersten k.Beide sind O(n): Jeder Zeiger geht einmal über die Liste.
Sieh es dir an
Ein Spiegel-Gutscheincode
Die Aktion „Spiegelcode“ akzeptiert nur Gutscheincodes, die sich von beiden Enden gleich lesen. Die Zeiger beginnen an beiden Enden und vergleichen Zeichen für Zeichen, bis sie sich treffen:
function isMirror(code) { let lo = 0; let hi = code.length - 1; while (lo < hi) { if (code[lo] !== code[hi]) { return false; } lo++; hi--; } return true;}console.log(isMirror("AB7CC7BA"));console.log(isMirror("AB7CD7BA"));truefalseGenauer, für Neugierige
Dieselbe Prüfung lässt sich in einer Zeile schreiben, indem man den Code mit einer umgedrehten Kopie vergleicht:
const code = "AB7CC7BA";console.log(code === [...code].reverse().join(""));true[...code].reverse().join("") erzeugt ein Array der Zeichen, dreht es um und fügt es zu einem neuen String zusammen.Die Antwort ist dieselbe, und es ist immer noch O(n). Der Unterschied: Die Kopie sind n Zeichen mehr Speicher und wird immer ganz gemacht, während die Zeiger beim ersten Unterschied anhalten. Bei einem Gutscheincode mit 8 Zeichen spielt das keine Rolle. Bei der Prüfung von Millionen Zeilen in einem Log schon.
Dieselben Zeiger drehen ein Array an Ort und Stelle um: lo und hi vertauschen, nach innen bewegen.
Übung
Am nächsten an der Karte
Löse die Aufgabe in deinem Editor.
Am nächsten an der Karte. Es gibt nicht immer ein genaues Paar. Dann bietet der Shop die zwei Produkte an, die zusammen der Karte am nächsten kommen, ohne sie zu überschreiten. Die Preisliste ist 3 €, 5 €, 8 €, 12 €, 15 €, 19 €, 24 €, 27 €, 31 € und 36 €, und die Karte hat 45 €. Mit zwei Zeigern: Liegt die Summe innerhalb der Karte, merke sie dir, wenn sie besser ist als die bisher beste, und bewege lo. Sonst bewege hi.
Am nächsten an 45 €: 8 € + 36 € = 44 €Weitere Aufgaben
- Ein Spiegelcode mit Bindestrichen. Die Gutscheincodes werden mit Bindestrichen gedruckt:
AB7-CC-7BA. Überspringe die Bindestriche: Jeder Zeiger springt vor dem Vergleich über sie hinweg. - Zwei pro Fahrt. Die Pakete sind nach Gewicht sortiert, und eine Kurierfahrt trägt höchstens zwei Pakete mit zusammen bis zu 30 kg. Wie viele Fahrten braucht man mindestens? Leg das leichteste mit dem schwersten zusammen, wenn sie passen, sonst fährt das schwerste allein.
- Gemeinsame Produkte. Zwei sortierte Listen von Produktnummern: aus dem Lager und aus der Bestellung. Finde mit zwei Zeigern in dieselbe Richtung, welche Nummern in beiden stehen.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Binäre Suche · Mergesort und „Teile und herrsche“ · Stabiles Sortieren und die eingebaute Sortierung · Hashtabelle
- Geht weiter in: Gleitendes Fenster · Eine Hashtabelle als Zähler
- Aufgaben zum Thema: Techniken zum Lösen von Aufgaben