-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathBinarySearchSet.php
310 lines (283 loc) · 9.97 KB
/
BinarySearchSet.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
<?php
/**
* A collection that contains no duplicate elements. More formally, sets contain no
* pair of elements $e1 and $e2 such that $e1->equals($e2).
* As implied by its name, this class models the mathematical set abstraction.
* @author Azeem Michael
*/
class BinarySearchSet implements SetInterface
{
const COMPARABLE_MODE_CASE_INSENSITIVE = 0;
const COMPARABLE_MODE_CASE_SENSITIVE = 1;
/**
* @var \SplDoublyLinkedList $data
*/
private $data;
/**
* @var bool
*/
private $mode;
/**
* @param mixed $collection
* @throws \InvalidArgumentException
*/
public function __construct($collection = null)
{
$this->data = new \SplDoublyLinkedList();
if ($collection !== null) {
if ($collection instanceof \SplDoublyLinkedList || is_array($collection)) {
foreach ($collection as $element) $this->add($element);
} else {
throw new \InvalidArgumentException(
'Unsupported argument supplied for BinarySearchSet::__constructor(). Expected array|SplDoublyLinkedList but found '.
(is_object($collection) ? get_class($collection) : gettype($collection))
);
}
}
$this->mode = self::COMPARABLE_MODE_CASE_INSENSITIVE;
}
/**
* The class destructor
*/
public function __destructor()
{
unset($this);
}
/**
* The copy constructor
*/
public function __clone()
{
$this->data = clone $this->data;
}
/**
* Return a string representation of this set.
* @return string A string that contains the contents of the set.
*/
public function __toString()
{
$result = '(';
for($this->data->rewind(); $this->data->valid(); $this->data->next()) {
$result .= "{$this->data->current()} ";
}
$result = rtrim($result);
return $result .= ')';
}
/**
* Remove all the values from this collection so that it becomes empty.
*/
public function clear()
{
$this->data = new \SplDoublyLinkedList();
}
/**
* @return int the number of objects in this collection
*/
public function count()
{
return $this->data->count();
}
/**
* If this collection is empty, return true otherwise return false.
* @return boolean A boolean indicating if this collection is or is not empty.
*/
public function isEmpty()
{
return $this->data->isEmpty();
}
/**
* If this collection contains the given value return true, otherwise return false.
* @param mixed $element The value being searched for in this collection.
* @return boolean A boolean indicating if this collection contains the value.
*/
public function contains($element)
{
return $this->find($element) >= 0;
}
/**
* Remove the given item from this collection. If the item is not a member of this
* collection the operation fails.
* @param mixed $element value to be removed from the set
* @return boolean true if the value is removed, false if it is not removed.
*/
public function remove($element)
{
$foundAt = $this->find($element);
if ($foundAt >= 0) {
$this->data->offsetUnset($foundAt);
return true;
}
return false;
}
/**
* Create and return an iterator for this collection.
* @return Iterator an iterator for this collection.
*/
public function iterator()
{
return new BinarySearchIterator($this->data);
}
/**
* Insert a value into this set if it is not already present.
* @param mixed $element the object being added to the set
* @return boolean true if the object is added, false if it is not added.
*/
public function add($element)
{
$foundAt = $this->find($element);
if ($foundAt < 0) { // item not in list
$pos = -$foundAt - 1; //position where item should be inserted
$this->data->add($pos, $element);
return true;
}
return false;
}
/**
* Test to see if this set has the same contents as the given set.
* @param SetInterface $otherSet the set that is being compared to this set.
* @return boolean true if the two sets have the same contents, false otherwise.
*/
public function equals(SetInterface $otherSet)
{
if ($otherSet === $this) {
return true;
}
if (!($otherSet instanceof SetInterface)) {
return false;
}
if ($this->count() !== $otherSet->count()) {
return false;
}
for ($otherSet->rewind(); $otherSet->valid(); $otherSet->next()) {
if (!$this->contains($otherSet->current())) {
return false;
}
}
return true;
}
/**
* Create a new set whose contents will be the union of the this set
* and the given set.
* @param SetInterface $otherSet the set whose contents are being added to this set to form the union.
* @return SetInterface a new set that contains the union of the two sets.
*/
public function union(SetInterface $otherSet)
{
$tmp = clone $otherSet;
for ($this->data->rewind(); $this->data->valid(); $this->data->next()) {
$tmp->add($this->data->current());
}
return $tmp;
}
/**
* Create a new set whose contents will be the intersection of this
* set and the given set.
* @param SetInterface $otherSet the set whose contents are being intersected with this set.
* @return SetInterface a new set that contains the intersection of the two sets.
*/
public function intersection(SetInterface $otherSet)
{
$tmp = new BinarySearchSet();
for ($this->data->rewind(); $this->data->valid(); $this->data->next()) {
$hold = $this->data->current();
if ($otherSet->contains($hold)) {
$tmp->add($hold);
}
}
return $tmp;
}
/**
* Create a new set whose contents will be the set difference of this set and
* the given set. An item will be in the set difference if and only if it is
* in this set and not in the given set.
* @param SetInterface $otherSet the set being used to form the difference with this set.
* @return SetInterface a new set that contains the difference of the two sets.
*/
public function difference(SetInterface $otherSet)
{
$tmp = new BinarySearchSet();
for ($this->data->rewind(); $this->data->valid(); $this->data->next()) {
$hold = $this->data->current();
if (!$otherSet->contains($hold)) {
$tmp->add($hold);
}
}
return $tmp;
}
/**
* Test to see if the given set is a subset of this set.
* @param SetInterface the set that is being tested.
* @return boolean true if the given set is a subset of this set, false otherwise.
*/
public function hasSubset(SetInterface $otherSet)
{
for ($otherSet->rewind(); $otherSet->valid(); $otherSet->next()) {
$hold = $otherSet->current();
if (!$this->contains($hold)) {
return false;
}
}
return true;
}
/**
* Performs binary search on collection. If item is not in the list, it returns a signal that gives the
* location where the item should be inserted into the list.
* @param mixed $item the needle to search for
* @return int returns the position of the item, if item is not in the list,
* returns signal that gives location where it should be inserted
* @throws \InvalidArgumentException only ComarableInterface|string|int|float allowed
*/
private function find($item)
{
$low = 0;
$high = $this->data->count() - 1;
while ($low <= $high) {
$mid = ($low + $high) >> 1; //shift the bits of ($low+$hight) result to the right (each step means "divide by two")
$tmp = $this->data->offsetGet($mid);
if ($item instanceof ComparableInterface) {
$result = $this->getComparableMode() === self::COMPARABLE_MODE_CASE_SENSITIVE
? $item->compareTo($tmp)
: $item->compareToIgnoreCase($tmp);
} elseif (is_int($item) || is_float($item)) {
if ($item < $tmp) $result = -1;
elseif ($item > $tmp) $result = 1;
else $result = 0;
} elseif (is_string($item)) {
$result = $this->getComparableMode() === self::COMPARABLE_MODE_CASE_SENSITIVE
? strcasecmp($item, $tmp)
: strcmp($item, $tmp);
} else {
throw new \InvalidArgumentException('BinarySearch::find only accepts ComarableInterface|string|int|float, '.gettype($item) . ' given.');
}
if ($result === 0) //item has been found, return its location
return $mid;
if ($result < 0) //item comes before middle elements, search the top of the table
$high = $mid - 1;
else //item comes after the middle elements, search the bottom of the table
$low = $mid + 1;
}
//item is not in the list, return a signal that gives the location where
//the item should be inserted into the list.
return -$low - 1;
}
/**
* Set the comparable mode. There are two sets of modes that can be set:
* <ul>
* <li>COMPARABLE_MODE_CASE_INSENSITIVE</li>
* <li>COMPARABLE_MODE_CASE_SENSITIVE</li>
* </ul>
* The default mode is COMPARABLE_MODE_CASE_INSENSITIVE
* @param bool $mode
*/
public function setComparableMode($mode)
{
$this->mode = $mode;
}
/**
* @return bool
*/
public function getComparableMode()
{
return $this->mode;
}
}