De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf ·...

16
De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari 2015 Inleiding Dit artikel vergelijkt enkele algoritmen om de doorsnede van twee verzamelingen of rijen van getallen te vinden. In een rij kunnen elementen meerdere keren voorkomen; we spreken in plaats van sets over bags of multisets — de Nederlandse term is mij niet bekend — die zonder ex- tra moeite verwerkt kunnen worden. Het maakt een uitstap naar het probleem van getallen sor- teren.  I. Zoeken met geneste lussen Zet beide verzamelingen in twee rijen (arrays of lists). Loop elk element van verzameling B af om te zien of het eerste element van A daarin voorkomt, doe hetzelfde met het tweede element van A, etc. De meeste besproken algoritmen werken ook voor rijen getallen waarin elementen meer dan eens kunnen voorkomen. Het probleem wordt opgelost in O(N 2 ) stappen. Vanwege de eenvoud gebruiken we deze metho- de soms voor kleine verzamelingen.  II. Vectoren van bits In de programmeertaal Pascal is het berekenen van de doorsnede of vereniging van twee verza- melingen een ingebouwde bewerking. Deelverzamelingen van een domein worden gerepresen- teerd door vectoren van bits: als het i e  bit hoog is dan bevat de verzameling dat element. Sorteren is hiermee niet nodig. Voor deelverzamelingen van grote domeinen kost deze methode veel tijd en geheugen, namelijk O(#D), waarin #D de grootte van het domein is. Als we overstappen op vectoren van gehele van gehele getallen dan kunnen we daarmee een eenvoudig sorteeralgoritme vormen. Initialiseer een array A ter grootte van #D met nullen en lees de getallen in: als je een waarde i vindt, dan hoog je A i  op. Daarna loop je het array af en als A i  = c dan druk je c keer het getal i af. De rekentijd is in de orde van O(#D + N) stappen; het be- nodigde geheugen kan onpraktisch zijn als D bijvoorbeeld uit de 32-bits gehele getallen bestaat.  III. Sorteren van arrays Het originele quicksort algoritme van C.A.R. Hoare sorteert een rij getallen in situ. Het belang- rijkste onderdeel is de functie partition(). Die kiest eerst een getal uit de rij, de pivot en laat de in- dex variabelen i en j vanaf het begin en eind naar elkaar toe lopen en zoekt naar paren getallen A i  > pivot en A j  < pivot en verwisselt die telkens van plaats. Als i >= j, dan wordt de positie van de pivot geretourneerd en de quicksort() functie wordt re - cursief aangeroepen op het deelinterval met de getallen <= pivot en het deel met getallen >= pi- vot. Getallen die gelijk zijn aan de pivot kunnen dus aan beide zijden voorkomen, wat de imple- mentatie vereenvoudigt.

Transcript of De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf ·...

Page 1: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

De doorsnede van twee verzamelingen vindenDaniel von Asmuth

12 februari 2015

InleidingDit artikel vergelijkt enkele algoritmen om de doorsnede van twee verzamelingen of rijen van getallen   te  vinden.   In  een rij  kunnen elementen meerdere  keren  voorkomen;  we spreken   in plaats van sets over bags of multisets — de Nederlandse term is mij niet bekend — die zonder ex­tra moeite verwerkt kunnen worden. Het maakt een uitstap naar het probleem van getallen sor­teren. 

 I. Zoeken met geneste lussenZet beide verzamelingen in twee rijen (arrays of lists). Loop elk element van verzameling B af om te zien of het eerste element van A daarin voorkomt, doe hetzelfde met het tweede element van A, etc. De meeste besproken algoritmen werken ook voor rijen getallen waarin elementen meer dan eens kunnen voorkomen.

Het probleem wordt opgelost in O(N2) stappen. Vanwege de eenvoud gebruiken we deze metho­de soms voor kleine verzamelingen.

 II. Vectoren van bitsIn de programmeertaal Pascal is het berekenen van de doorsnede of vereniging van twee verza­melingen een ingebouwde bewerking. Deelverzamelingen van een domein worden gerepresen­teerd door vectoren van bits: als het ie bit hoog is dan bevat de verzameling dat element. 

Sorteren is hiermee niet nodig. Voor deelverzamelingen van grote domeinen kost deze methode veel tijd en geheugen, namelijk O(#D), waarin #D de grootte van het domein is.

Als we overstappen op vectoren van gehele van gehele getallen dan kunnen we daarmee een eenvoudig sorteeralgoritme vormen. Initialiseer een array A ter grootte van #D met nullen en lees de getallen in: als je een waarde i vindt, dan hoog je Ai op. Daarna loop je het array af en als Ai = c dan druk je c keer het getal i af. De rekentijd is in de orde van O(#D + N) stappen; het be­nodigde geheugen kan onpraktisch zijn als D bijvoorbeeld uit de 32­bits gehele getallen bestaat. 

 III. Sorteren van arraysHet originele  quicksort algoritme van C.A.R. Hoare sorteert een rij getallen  in situ. Het belang­rijkste onderdeel is de functie partition(). Die kiest eerst een getal uit de rij, de pivot en laat de in­dex variabelen i en j vanaf het begin en eind naar elkaar toe lopen en zoekt naar paren getallen Ai > pivot en Aj < pivot en verwisselt die telkens van plaats.

Als i >= j, dan wordt de positie van de pivot geretourneerd en de quicksort() functie wordt re­cursief aangeroepen op het deelinterval met de getallen <= pivot en het deel met getallen >= pi­vot. Getallen die gelijk zijn aan de pivot kunnen dus aan beide zijden voorkomen, wat de imple­mentatie vereenvoudigt.

Page 2: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

Figuur 1. Het deelarray s..m wordt gepartitioneerd

Iets moeilijker wordt het als we een array in drie delen willen partitioneren, met links de waar­den groter dan, rechts die kleiner dan in het midden die gelijk zijn aan de pivot. Een eenvoudige keuze voor de pivot is midden in het array. E.W. Dijkstra noemde dit het Dutch National Flag Pro­blem. Het is soms nodig om het middensegment op te schuiven, wat deze oplossing iets langza­mer maakt. Dat kan worden voorkomen door de mediaan te gebruiken als pivot, wat in theorie O(N) stappen kost. 

Figuur 2. Partitioneren in drie delen

 IV. De doorsnede van twee ongesorteerde rijenDe quicksort() functie moet daarna de linker en rechter deelrijen sorteren en kan het midden la­ten staan, een besparing die niet opweegt tegen het extra werk voor de driedeling. Dit algoritme kan zodanig worden aangepast, dat de doorsnede wordt bepaald, terwijl we de twee arrays sor­teren. 

De intersect() functie van listing I roept zichzelf recursief aan om de linker en rechter deelrijen te sorteren, en vergelijkt het verschil tussen f en g om te bepalen hoe vaak de pivot waarde voor­komt. De pivot is hier het gemiddelde van de hoogste en laagste waarde; omdat die niet in A en B hoeft voor te komen bewaren we de de waarden van Af en Bg in As en Bs.

Dit lost het probleem op in O(N.log(N)) stappen: een goede keus als de invoergegevens ongesor­teerd zijn. Een nadeel van de aanpassing is dat de rijen na afloop niet volledig gesorteerd zijn, maar daarvoor besparen we iets tijd door af en toe getallen over te slaan. 

V. Drie algoritmen om lijsten sorterenWe beginnen met een variant op het  quicksort  algoritme, waarin de te sorteren getallen in een linked list worden opgeslagen. Terwijl de sleutels aan de lijst worden toegevoegd zien we kans om het grootste en kleinste getal in de lijst te bepalen. We kiezen het gemiddelde van die twee als pivot. 

Een functie genaamd partition() haalt telkens een element uit de lijst en als de waarde kleiner of gelijk aan de pivot is, wordt het aan de linker lijst toegevoegd, en als het groter is aan de rechter.  Merk op dat het splitsen van een lijst vrij gemakkelijk is. De quicksort() functie bepaalt de pivot en roept partition() aan om de lijst op te splitsen. Daarna roept ze zichzelf recursief aan om de beide deellijsten te sorteren en voegt ze samen tot één gesorteerde lijst. De oude pivot is daarbij maximum of minimum van de deellijst; dat zal net zo goed zijn als een willekeurige waarde uit de lijst.

Een probleem ontstaat als alle te sorteren getallen dezelfde waarde hebben: alle getallen belan­den in de linker deelrij en het algoritme stopt nooit; dat kan worden opgelost met een derde lijst 

>= pivot

s mi j

<= pivot ?

> pivot

s mf g

< pivot = pivot

i j

Page 3: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

voor getallen die precies gelijk zijn aan de pivot. Een andere mogelijkheid is om de pivots beur­telings in de linker en rechter lijst te stoppen. 

Een lijst van N = 2n getallen wordt gesplitst in twee lijsten van 2n­1 getallen, vier lijsten van 2n­2, enzovoorts, tot er na n = log(N) doorgangen nog N × 1 overblijven, waardoor de rekentijd op O(N×log(N)) uitkomt.

Dit algoritme kan worden versneld door een lijst niet op te splitsen in twee helften, maar door de getalswaarden te verdelen over een array van N lijsten; in plaats van te vergelijken met een pivot delen we het interval tussen de grootste en kleinste waarde op in gelijke delen. 

Een lijst van N = 2n getallen wordt gesplitst in delen van elk 2n/2 getallen, gevolgd door 2n/4, en­zovoorts, tot er na n = log(log(N)) doorgangen nog N × 20 overblijven, waardoor de rekentijd op O(N×log(log(N))) uitkomt, ten koste van extra geheugengebruik.

Nog meer snelheid wordt gewonnen met een array van N gelinkte lijsten, waardoor elke lijst ge­middeld 1 element krijgt. 

Merk op dat we de mediaan van een array in O(N) kunnen vinden met Hoares quickselect algorit­me, dat de rij eerst partitioneert en dan1 partitie verder te doorzoeken.

VI.AnalyseHet laatste algoritme kost meer dan O(N) rekenstappen als we aannemen dat een bucket met 0 elementen toch 1 stap kost om te inspecteren, met 1 element ook 1 stap en 2 elementen in 2 stap­pen, en we voor grotere aantallen de partitionering recursief uitvoeren.

We beginnen met een uniforme kansverdeling; als model kun je denken aan N worpen met een 'dobbelsteen' met N zijden. De verzameling van NN verschillende uitkomsten gaan we onderver­delen in partities, die aangeven hoe vaak getal voorkomt. Voor N = 4 bestaan er 5 partities:

● [1 1 1 1]● [1 1 2]● [2 2]● [1 3]● [4]

De partitie [1 1 1 1] bevat alle permutaties van {1,2,3,4}, terwijl de partitie [4] bestaat uit {(1,1,1,1), (2,2,2,2), (3,3,3,3), (4,4,4,4)} en [1 1 2] bestaat uit alle permutaties waarin één cijfer dubbel voor­komt. Een partitie is ook te beschouwen als een multiset van positieve gehele getallen waarvan de som N bedraagt.

Op de zelfde manier waarop we hierboven bij een permutatie een partitie vonden, kunnen we aan een een partitie een klasse toevoegen. 

● [1 1 1 1] → [4]● [1 1 2]  → [1 2]● [2 2]  → [2]● [1 3]  → [1 1]● [4]  → [1]

Verder duiden we het aantal cijfers in een partitie aan met P. Het aantal permutaties van een par­titie p is nu gelijk aan het aantal deelverzamelingen van {1..N} met P elementen, vermenigvul­digd met het aantal permutaties van p, vermenigvuldigd met het aantal permutaties van de klasse k. Hierin is het aantal deelverzamelingen gelijk aan

Het aantal permutaties van een partitie of klasse [x y z] kunnen we berekenen als 

NP =N!

N−P !P !

Page 4: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

Zo horen bij de partitie [1 1 2] de 4 deelverzamelingen {1, 2, 3}, {1,2,4}, {1,3,4} en {2,3,4}. De klasse [1 2] heeft 3 permutaties, zodat bij deelverzameling {1,2,3} de reeksen (1,1,2,3), (1,2,2,3) en (1,2,3,3) horen,   die   elk   weer   op   12   manieren   gepermuteerd   kunnen   worden,   bijvoorbeeld   (1,1,2,3), (1,1,3,2), (1,2,1,3), (1,2,3,1), (1,3,1,2), (1,3,2,1), (2,1,1,3), (2,1,3,1), (2,3,1,1), (3,1,1,2), (3,1,2,1), (3,2,1,1).

We kunnen hiermee berekenen hoe groot de kans is dat een bucket gevuld is met M getallen. Ie­dere permutatie van een partitie [x y z] levert 1 uitkomst bij aan de buckets met x getallen, 1 uit­komst aan de buckets met y getallen, 1 maal aan de buckets met z getallen en N – 3 maal aan de lege buckets. 

Zoals te verwachten blijken buckets met 1 getal erin het meest voor te komen. We ontdekken een regelmaat die uitgedrukt kan worden als 

Verder zien we dat de kans P(2) = P(1) / 2, plus wat meer buckets  met 0 getallen dan met 2 erin. De kans op meer dan 2 getallen in een bucket loopt snel terug.

Op deze manier hebben we van de uniforme kansverdeling van de sleutels een kansverdeling af­geleid voor het aantal getallen in een bucket, die een scherpe piek vertoont. Voor niet­uniforme verdelingen, zoals de normale kansverdeling, laat zich aanzien dat het aantal buckets met meer dan 2 elementen groter is. Als we van deze kansverdeling het gemiddelde over alle niet­lege buckets nemen, dan krijgen we een functie die heel langzaam groeit met N.

De gemiddelde rekentijd t(N) kan uit de kansverdeling worden berekend door aan te nemen dat een bucket met M elementen vanwege de recursie 1 + t(M) kost voor M in 2..N en 1 stap voor M = 0 of 1.  We krijgen dan een recurrente betrekking die we iteratief kunnen berekenen.

De functie zal met N langzamer stijgen dan t(N) = log(log(N)), zie de grafiek hieronder Het sor­teeralgoritme is te vergelijken met radix sort, dat een array van gehele getallen sorteert in O(N) stappen.

VII.HashingHashing kan sneller zijn dan sorteren. Daarvoor kiezen het aantal  hash buckets bijvoorbeeld op de wortel van #B (als #A > #B) en verdelen de rij A daarover en verdelen de rij B over een even grote hash tabel.

Daarna moet je de doorsneden bepalen tussen de inhoud van corresponderende paren buckets, bijvoorbeeld met algoritme III. Als tenminste één van de buckets leeg is, kun je die overslaan. Dit algoritme zal Ω(N) rekenstappen vergen, maar de output is ongesorteerd.  

Ook het bovengenoemde algoritme om gelinkte lijsten te sorteren kan worden aangepast zodat verzamelingen A en B elk over een rij van gelinkte lijsten worden verdeeld, die dan paarsgewijs verder worden gesorteerd en geteld. Hiermee kan de doorsnede worden bepaald in bijna lineai­re rekentijd. 

t N =

∑M=0

N

π M ⋅1t M

∑M=0

N

π M

P N=N−1N N−1

π N =N !x!y! z!

Page 5: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

Figuur 3. Rekentijd voor sorteren rij van gelinkte lijsten

VIII.Twee gesorteerde rijen sequentieel vergelijkenAls de rijen A en B al gesorteerd zijn, dan kan de doorsnede eenvoudig worden gevonden door telkens de kleinste elementen van beide rijen te vergelijken: als ze gelijk zijn rapporteer je het ge­tal en verwijdert beide elementen, anders verwijder je het kleinste en vergelijkt weer de twee kleinste, enzovoorts.

Dit lost het probleem eenvoudig op in O(N) stappen. Voor multisets kan het algoritme worden versneld met een array waarin per positie de waarde van het element wordt opgeslagen plus het aantal malen dat ze voorkomt. 

IX. Binaire zoekbomenDe representatie met vectoren van bits verspilt geheugen wanneer het domein groot is en de verzamelingen kleiner; in zo'n geval zijn gelinkte lijsten beter, maar binaire bomen zijn sneller te doorzoeken. Omdat het domein bekend is, is het niet nodig om de zoeksleutels expliciet op te slaan; een waarde correspondeert met een vaste positie in de boom, zodat diens voorouders ook een knoop beslaan wanneer ze 0 keer voorkomen. Dat kost per getal maximaal log(#D) knopen aan geheugenruimte, met een maximum van O(#D) voor de hele boom.

In plaats van een bit, gebruiken we een integer veld om te tellen hoe vaak een getal in de ver­zameling voorkomt. De broncode is te vinden in listing 2.

Stelling I. De functie intersect()  drukt alle elementen van de doorsnede van boom A en boom B éénmaal af in stijgende volgorde.

Page 6: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

We geven een synopsis van het bewijs. De functie intersect() bestaat uit niets meer dan een aanroep van intersect_r( tree_a, tree_b, 0, M­1) voor het domein [0..M­1].

A. Als  één (deel)verzameling leeg is, dan is de doorsnede van A en B ook leeg.

Het if­statement zorgt in dat geval dat de functie geen uitvoer produceert.  if( node1 == NULL || node2 == NULL)    return;

B. De bomen zijn zo opgezet dat knopen steeds corresponderen met dezelfde zoeksleutel. De node1 en node2 parameters verwijzen bij de eerste aanroep naar de beide wortels, bij de eerste recursieve aanroep naar diens linker zoon, de tweede recursieve aanroep naar diens rechter zoon. Dus verwijzen de parameters altijd naar de zelfde positie in de bomen.

C. Als de doorsnede van de deelbomen, waarvan node1 en node2 de wortels zijn, niet leeg is, dan zorgt het for­statement dat de functie de juiste uitvoer produceert als het om twee bladen gaat en geen uitvoer als een interne knoop bij is.  for( i = 0; i < min( node1­>count, node2­>count); i++)    printf( "Gevonden: \t%05d \n", node_key);

D. De intersect_r() functie vergelijkt eerst de linker zonen, dan de knopen node1 en node2 zelf en vervolgens de rechter zonen.   intersect_r( node1­>left, node2­>left, lo, node_key – 1);  for( i = 0; i < min( node1­>count, node2­>count); i++)    printf( "Gevonden: \t%05d \n", node_key);  intersect_r( node1­>right, node2­>right, node_key + 1, hi

E. De intersect_r() functie drukt eerst de doorsnede van de linker deelbomen af, dan van de wortels van de deelbomen en vervolgens van de rechter deelbomen. 

Dit volgt met structurele inductie op bewering D, samen met de beweringen A, B, en C, die er­voor zorgen dat elk element van de doorsnede precies één keer wordt afgedrukt.

F. De intersect_r() functie drukt elementen van de doorsnede op volgorde af. De parameters lo en hi zijn de onder­ en bovengrenzen van de deelbomen en de wortel deelt dat interval in drie delen, waarvan eerst het interval [0..node_key­1] wordt verwerkt, dan node_key zelf, gevolgd door [node_key+1, hi]. Dit volgt uit de code onder bewering D.  int           node_key        = min(max(hi / 2 + lo / 2 + 1, lo), hi);

G. Het domein [0..M] wordt zodanig onderverdeeld, dat elk element correspondeert met pre­cies één knoop in de zoekboom.

Voor interval [0..0] is de sleutel 0  en wordt intersect_r() aangeroepen op de delen [0..­1] en [2..0]. Dat zijn ongeldige waarden en de NULL­test zal die afvangen. 

Voor interval  [0..1]   is  de sleutel  1 en wordt  intersect_r()  aangeroepen op de delen [0..0]  en [2..0]. Het laatste is ongeldig en wordt door de NULL­test afgevangen. 

Voor interval  [0..2]   is  de sleutel  2  en wordt  intersect_r()  aangeroepen op de delen [0..1]  en [3..2]. Het laatste is ongeldig en wordt door de NULL­test afgevangen. 

Voor  interval  [3..3]   is de sleutel 3 en wordt intersect_r()  aangeroepen op de delen [3..2]  en [4..3]. Die zijn ongeldig en worden door de NULL­test afgevangen. 

Voor interval  [0..3]   is  de sleutel  2  en wordt  intersect_r()  aangeroepen op de delen [0..1]  en [3..3]. 

De bewering G klopt dus voor M = 0, 1, 2 en 3; inductie bewijst ze voor alle waarden van M.

Stelling II. Een deelverzameling van domein D met  e  elementen kan worden gerepresen­teerd door een binaire boom met ten hoogste 2∙e knopen. 

Page 7: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

Als er e bladen zijn en e is een macht van 2, dan heeft de zoekboom 1 + 2 + 4 + 8 + ... + e/2 = e  – 1 interne knopen nodig. In ons geval kunnen interne knopen eveneens elementen van de ver­zameling   representeren.   Voor   multisets   worden   alle   elementen   met   dezelfde   waarde   in   1 knoop opgeslagen en in één rekenstap verwerkt.

Stelling III. Algoritme IX bepaalt de doorsnede van twee verzamelingen in O( #( A∩B )) re­kentijd. 

Voor stelling I hebben we laten zien dat elk blad en elke interne knoop die nodig is voor een binaire boom van het resultaat één keer verwerkt wordt en in stelling II dat er minder interne knopen dan bladen nodig zijn. Voor elk blad in het resultaat zal intersect_r() tweemaal worden aangeroepen met een NULL parameter. De kosten van de if­test vergroten de tijd voor de ver­werking van het blad maar weinig, dus is de stelling geldig. 

Het opbouwen van de zoekboom kost O(N.log(N)) stappen, maar als de invoer gesorteerd is reduceert dat tot O(N). Eventueel is het mogelijk om het sorteren en opbouwen te combineren. 

X. Gesorteerde rijenHierboven gebruikten we een boom waarin de getalswaarden impliciet uit de locatie volgen, als we daarentegen de locatie van de elementen vastleggen, krijgen we een array; als beide impliciet zijn, komen we uit op de bit vectoren van paragraaf II. 

De broncode is te vinden in listing 3. Dit algoritme is generieker dan het vorige. De zoeksleutels zijn niet van te voren vastgelegd, zodat geen geheugenruimte aan lege knopen verspild wordt. Het voornaamste verschil met algoritme VIII is dat de rij niet sequentieel wordt doorlopen, maar telkens wordt opgedeeld in twee delen van gelijke lengte. 

Als de zoeksleutel voor rij A kleiner is dan die voor rij B, dan kunnen we kleinere waarden dan akey vinden in de linker deelbomen van A en B, maar voor grotere waarden moeten we de rech­ter deelboom van A vergelijken met de gehele boom B; alleen als beide sleutels aan elkaar gelijk zijn kunnen beide bomen worden opgesplitst. 

De parameters xmin en xmax worden gebruikt om de zoektocht op tijd te beëindigen. De manier waarop dit interval recursief wordt gesplitst garandeert dat elke sleutelwaarde één keer wordt verwerkt; aanroepen van intersect() met overlappende array indices worden afgevangen door de if­statements aan het begin. Die zorgen er tevens voor dat een deelboom van A niet verder wordt afgelopen als de deelboom van B al leeg is en omgekeerd, zodat de rekentijd van dit al­goritme eveneens lineair toeneemt met de grootte van de uitvoer. 

Tests met algoritme IX geven aan dat het aantal recursieve aanroepen ongeveer 150 % van de grootte van de verzameling bedraagt, tegen 500 % voor algoritme X, terwijl ze ongeveer even­veel rekentijd kosten. Een oorzaak ligt in de malloc() functie om geheugen voor de boom te re­serveren. 

Deze oplossing  lijkt  sterk op het  divide­and­conquer­merge  van V.J.  Duvanenko (http://www.­drdobbs.com/parallel/parallel­merge/229204454);  hier de binaire zoek­functie  in de hoofdre­cursie ingebouwd. Duvanenko bereikt daarmee toch een lineaire rekentijd, want als element i in O(log(n)) stappen wordt verwerkt, dan kost de gehele verzameling 

t N = ∑i=1

logN i⋅N2 i

=2⋅N

Page 8: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

ConclusiesMet de algoritmen IX en X uit dit artikel is het mogelijk is het mogelijk om de doorsnede van twee verzamelingen te bepalen in lineaire rekentijd. Voor non­uniform verdeelde zoeksleutels zal het iets sneller gaan. Bovengenoemde methoden vereisen dat de invoer gesorteerd is; als dat niet het geval is zorgen de algoritmen IV en VII en passant voor die sortering.

De algoritmen IV, IX en X zijn alle drie gebaseerd op het recursief opsplitsen van een interval met verschillende oplossingen voor het probleem dat de mediaan van A niet die van B is. Uit de analyse van paragraaf VI volgt, dat als we de arrays elk verdelen over N buckets, een groot deel van de paren 0 elementen in bucket Ai of Bi zal bevatten, zodat we het antwoord vinden zonder alle elementen te testen. De overgeslagen elementen zijn te vinden in de bladen van de zoekbo­men: een niveau hoger hebben we buckets met gemiddeld 2 elementen, zodat de kans op een lege al veel kleiner is, dan N/4,... Het resultaat is dat bijna de hele zoekboom doorlopen moet worden.

Veel hogere snelheden zijn mogelijk door de algoritmen IX en X zodanig aan te passen, dat de twee recursieve functieaanroepen parallel worden uitgevoerd; door de resultaten tijdelijk op te slaan kunnen ze zonder weer te sorteren op volgorde worden afgedrukt.

Page 9: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

Bijlagen

C programma code geschreven onder Linux

Listing 1. Rij sorteren en doorsnede bepalen 

#include <stdio.h>#include <stdlib.h>#include <limits.h>#include <time.h>

#define  N   40#define  M   60

#define min(a,b) (((a) < (b)) ? (a) : (b))#define max(a,b) (((a) > (b)) ? (a) : (b))

int             a[ N];int             b[ N];

/* vul rij met willekeurige waarden */static void fill_arrays( void){ int           i;

  srand( time( 0));  for( i = 0; i < N; i++)  { a[i] = rand() % M;  }  srand( time( 0) + 1);  for( i = 0; i < N; i++)  { b[i] = rand() % M;  }}

/* druk de rijen af */static void print_arrays(){ int           i;

  for( i = 0; i <= N ­ 1; i++)  { printf( "\t%05d\t%05d\n", a[i], b[i]);  }  printf( "\n");}

/* druk gemeenschappelijke elementen af */static voidreport( int pivot, int amin, int amax, int bmin, int bmax){  int           i;

  for( i = 0; i <= min( amax ­ amin, bmax ­ bmin); i++)    printf( "%05d\n", pivot);}

/* tel gemeenschappelijke elementen */static voidreport2( int pivot, int amin, int amax, int bmin, int bmax){  int           ac              = 0;  int           bc              = 0;  int           i;

  for( i = amin; i <= amax; i++)    if( a[i] == pivot)      ac++;  for( i = bmin; i <= bmax; i++)    if( b[i] == pivot)      bc++;

Page 10: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

  report( pivot, 1, ac, 1, bc);}/* zoek minimum en maximum waarden */static voidfind_min_max( int *a, int amin, int amax, int *lo, int *hi){ int           i;

  *lo = INT_MAX; *hi = INT_MIN;  for( i = amin; i <= amax; i++)  {    if( a[i] < *lo)      *lo = a[i];    if( a[i] > *hi)      *hi = a[i];  }}

/* verwissel twee getallen */static voidswap( int *a, int *b){  int           c;

  c = *a; *a = *b; *b = c;}

/* * Splits een rij in drie delen met waarden <, =, > pivot * In parms:    a:      het array *              pivot:  waarde om mee te vergelijken *              s:      ondergrens van interval *              m:      bovengrens van interval * Uit parms:   f:      laagste index met pivot *              g:      hoogste index met pivot */static voidpartition( int *a, int pivot, int s, int m, int *f, int *g){  int           i       = s;  int           j       = m;

  while( i < j)  {    /* elementen die op juiste plaats staan overslaan */    if( a[i] < pivot)      i++;    else      /* elementen verplaatsen */      if( a[i] == pivot && i < *f)        swap( &a[i], &a[­­(*f)]);    if( a[j] > pivot)      j­­;    else      if( a[j] == pivot && j > *g)        swap( &a[j], &a[++(*g)]);      else        if( a[i] > pivot && a[j] < pivot)          swap( &a[i++], &a[j­­]);

     /* testen of we klaar zijn */    if( a[(*g)+1] == pivot)      (*g)++;    if( i == *f && j == *g)      break;

    /* de pivots een positie opschuiven */    if( i == *f)    {      if( a[(*g)+1] <= pivot)        swap( &a[(*f)++], &a[++(*g)]);      else        if( a[j] < pivot)        {

Page 11: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

          swap( &a[*f], &a[j]);          while( a[*f] != pivot)            (*f)++;          if( *f > *g)            *g = *f;        }    }    else      if( j == *g)      {        if( a[(*f)­1] >= pivot)          swap( &a[(*g)­­], &a[­­(*f)]);        else          if( a[i] > pivot)          {            swap( &a[*g], &a[i]);            while( a[*g] != pivot)              (*g)­­;            if( *g < *f)              *f = *g;          }      }  }}

/* Bepaal de doorsnede van deelrijen tussen Amin en Amax, Bmin en Bmax *//* met lo en hi als laagste en hoogste waarden */static voidintersect( int *a, int amin, int amax, int *b, int bmin, int bmax, int lo, int hi){  int           f, g, ai, aj, bi, bj;  int           pivot, as, bs;

  /* tests voor geval dat we (bijna) klaar zijn */  if (amin > amax || bmin > bmax)    return;  if (amin == amax)  { report2( a[amin], amin, amax, bmin, bmax);    return;  }  else    if( bmin == bmax)    { report2( b[bmin], amin, amax, bmin, bmax);      return;    }

  /* De pivot wordt bepaald door lo en hi en hoeft zelf niet voor te komen */  as = bs = pivot = (lo + hi) / 2;  ai = aj = f = (amin + amax) / 2;  bi = bj = g = (bmin + bmax) / 2;

  /* partitioneer beide arrays */  if( amin < amax)  {    swap( &as, &a[f]);    partition( a, pivot, amin, amax, &ai, &aj);    if( as < pivot)      swap( &as, &a[ai++]);    if( as > pivot)      swap( &as, &a[aj­­]);  }  else    ai = aj = amin;  if( bmin < bmax)  {    swap( &bs, &b[g]);    partition( b, pivot, bmin, bmax, &bi, &bj);    if( bs < pivot)      swap( &bs, &b[bi++]);    if( bs > pivot)      swap( &bs, &b[bj­­]);  }  else

Page 12: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

    bi = bj = bmin;

  /* sorteer de arrays recursief */  intersect( a, amin, ai­1, b, bmin, bi­1, lo, pivot ­ 1);  report( pivot, ai, aj, bi, bj);  intersect( a, aj+1, amax, b, bj+1, bmax, pivot + 1, hi);}

int main( int argc, char **argv){  int alo, ahi, blo, bhi;

  fill_arrays();  print_arrays();  find_min_max( a, 0, N ­ 1, &alo, &ahi);  find_min_max( b, 0, N ­ 1, &blo, &bhi);  intersect( a, 0, N ­ 1, b, 0, N ­ 1, min( alo, blo), max( ahi, bhi));  exit( 0);}

Page 13: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

Listing 2. Doorsnede van twee binaire bomen

#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <time.h>

#define min(a,b) (((a) < (b)) ? (a) : (b))#define max(a,b) (((a) > (b)) ? (a) : (b))

#define  N   30 /* aantal elementen in verzameling */#define  M  100 /* maximum waarde van een element */

int             a[ N]; /* tijdelijke opslag voor getalswaarden */int             b[ N];

typedef         struct node{ int           count; /* hoevaak de waarde voorkomt */  struct node   *left; /* wijzer naar kleinere elementen */  struct node   *right; /* wijzer naar grotere elementen */} NODE, *NODE_P;

/* maak een nieuw element voor in de boom */static NODE_P create_node( void){  NODE_P        result;

  if((result = (NODE_P) malloc( sizeof( NODE))) == NULL)  { fprintf( stderr, "Malloc() failed \n");    exit( 1);  }  else  { result­> count = 0;    result­>left = result­>right = NULL;  }  return result;}

/* recursief deel van opbouwfunctie */static void build_tree_r( NODE_P *node_p, int *keys, int *i, int lo, int hi){  int           node_key        = min(max(hi / 2 + lo / 2 + 1, lo), hi);

  if( *i >= N || keys[*i] < lo || keys[*i] > hi)    return; /* test om recursie te termineren */

  if( *node_p == NULL)    *node_p = create_node(); /* voeg nieuwe knoop toe aan boom */  if( keys[*i] == node_key)  { (*node_p)­>count++; /* tel het element mee */    ++*i;  }  if( keys[*i] < node_key) /* getal te klein; zoek verder */    build_tree_r( &(*node_p)­>left, keys, i, lo, node_key ­ 1);  else    if( keys[*i] > node_key) /* getal te groot; zoek verder */      build_tree_r( &(*node_p)­>right, keys, i, node_key + 1, hi);

  build_tree_r( node_p, keys, i, lo, hi); /* ga verder met volgende getal */}

/* Bouw een zoekboom van de elementen van een gesorteerde rij */void build_tree( NODE_P *root, int *keys){  int           i       = 0;

  build_tree_r( root, keys, &i, 0, M ­ 1);}

/* Recursief deel van de bepaling van de doorsnede */static void intersect_r( NODE_P node1, NODE_P node2, int lo, int hi)

Page 14: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

{  int           node_key        = min(max(hi / 2 + lo / 2 + 1, lo), hi);  int           i;

  if( node1 == NULL || node2 == NULL) /* controleer of we al klaar zijn */    return;

  intersect_r( node1­>left, node2­>left, lo, node_key – 1);  /* het kleinste aantal malen dat element voorkomt bepaalt de doorsnede */  for( i = 0; i < min( node1­>count, node2­>count); i++)    printf( "Gevonden: \t%05d \n", node_key);  intersect_r( node1­>right, node2­>right, node_key + 1, hi);}

/* Druk de gemeenschappelijke elementen in twee bomen af */void intersect( NODE_P tree1, NODE_P tree2){  printf( "\nDe doorsnede bestaat uit: \n");  intersect_r( tree1, tree2, 0, M ­ 1);}

/* Vul rijen met willekeurige getallen */static void fill_arrays( void){ int           i;

  srand( time( 0));  for( i = 0; i < N; i++)  { a[i] = rand() % M;  }  srand( time( 0) + 1);  for( i = 0; i < N; i++)  { b[i] = rand() % M;  }}

/* een hulpfunctie voor quicksort */static int compare( const void *a, const void *b){ return *(int *)a ­ *(int *)b;}

/* het hoofdprogramma */int main( int argc, char **argv){  NODE_P        tree_a          = NULL;  NODE_P        tree_b          = NULL;

  fill_arrays(); /* vul geheugen met de getallen */  qsort( a, N, sizeof( int), compare);  qsort( b, N, sizeof( int), compare); /* sorteer de waarden eerst */  build_tree( &tree_a, a); /* om de bomen snel op te bouwen */  build_tree( &tree_b, b);

  intersect( tree_a, tree_b); /* bepaal de doorsnede */  return 0;}

Page 15: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

Listing 3. Doorsnede van twee gesorteerde rijen

#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <time.h>

#define  N      30#define  M      60

#define min(a,b) (((a) < (b)) ? (a) : (b))#define max(a,b) (((a) > (b)) ? (a) : (b))

int             a[ N];int             b[ N];

/* http://stackoverflow.com/questions/1903954/is­there­a­standard­sign­function­signum­sgn­in­c­c */int sgn(int val){  return (val > 0) ­ (val < 0);}

/* vul rijen met willekeurige getallen */void fill_arrays( void){ int           i;

  srand( time( 0));  for( i = 0; i < N; i++)    a[i] = rand() % M;  srand( time( 0) + 1);  for( i = 0; i < N; i++)    b[i] = rand() % M;}

/* hulpfunctie voor sorteren */static int compare( const void *a, const void *b){ return *(int *)a ­ *(int *)b;}

void print_arrays( void){ int           i;

  for( i = 0; i < N; i++)    printf( "%d:\t%05d\t%05d\n", i, a[i], b[i]);  printf( "\n");}

/* druk resultaat af */void found( int x){   if( x >= 0)    printf( "Found value\t%05d \n", x);}

/* bepaal de doorsnede van twee (deel)rijen A en B */static void intersect( int a[], int amin, int amax, int b[], int bmin, int bmax,  int xmin, int xmax){  int           ai              = min(max(amin / 2 + amax / 2 + 1, amin), amax);  int           bi              = min(max(bmin / 2 + bmax / 2 + 1, bmin), bmax);  int           aj              = ai;  int           bj              = bi;  int           akey            = a[ai]; /* de zoeksleutels */  int           bkey            = b[bi];  int           i;

  /* controleer of de deelrijen leeg zijn */  if((amin > amax) || (bmin > bmax))    return;  /* controleer of de waarden binnen de grenzen vallen */  if((a[amin] > xmax) || (a[amax] < xmin) || (b[bmin] > xmax) || (b[bmax] < xmin))

Page 16: De doorsnede van twee verzamelingen vinden - XS4ALLbakunin.xs4all.nl/artikelen/doorsnedes.pdf · 2015. 2. 13. · De doorsnede van twee verzamelingen vinden Daniel von Asmuth 12 februari

    return;    /* zoek getallen gelijk aan de sleutels */  while((ai > amin) && (a[ai­1] == akey))    ai­­;  while((aj < amax) && (a[aj+1] == akey))    aj++;  while((bi > bmin) && (b[bi­1] == bkey))    bi­­;  while((bj < bmax) && (b[bj+1] == bkey))    bj++;

  /* pas het algoritme recursief toe op de rest van het array */  switch( sgn( akey ­ bkey))  { case ­1: intersect( a, amin, aj,    b, bmin, bi­1,  xmin,   akey);             intersect( a, aj+1, amax,  b, bmin, bmax,  akey+1, xmax);             break;    case 1:  intersect( a, amin, ai­1,  b, bmin, bj,    xmin,   bkey);            intersect( a, amin, amax,  b, bj+1, bmax,  bkey+1, xmax);            break;    case 0: intersect( a, amin, ai­1,  b, bmin, bi­1,  xmin,   akey­1);            for( i = 0; i <= min( aj­ai, bj­bi); i++)              found( akey);       intersect( a, aj+1, amax,  b, bj+1, bmax,  akey+1, xmax);           break;  }}

int main( int argc, char **argv){  fill_arrays();  qsort( a, N, sizeof( int), compare);   /* standaard quicksort functie */  qsort( b, N, sizeof( int), compare);  print_arrays();  intersect(a, 0, N­1, b, 0, N­1, max(a[0],b[0]), min(a[N­1],b[N­1]));  exit( 0);}