Dual-Pivot Binary Search
vkostyukov.ru
vkostyukov.ru
1. Note that a 3 way pivoting (partitioning or decision) is EXACTLY the same as doing two consecutive 2-way pivoting. Check both functions. They have the exact same # of comparisons and index calculations.
2. Note that Quicksort and Quickselect (QS) and Binarysearch (BS) are very very similar. Quicksort recurses on BOTH partitions whereas QS and BS only recurse on ONE. This is important! You don't care if your pivot in Quicksearch is a little of. You still have to do the work anyways. For QS/BS you do care a lot since the other part gets immediately discarded!
3. Where 2) implies: Quicksort is harder to get non-recursive implementation. You still have to maintain some sort of stack even for the iterative part. Whereas for QS and BS it is very very easy to do an iterative version.
Now notice how you actually worsened the runtime complexity with your new approach:
1. A standard BS discards exactly half the array. Thus, after two iterations you discarded 3/4.
2. Your new algorithm's one iteration is the same as the default BS two iterations but you only remove 2/3.
Conclusion: It makes little sense (other than having fun) to apply this to BS/QS. Though, I like the complexity analysis and the overall post.
it's not about tuning something and getting gain (in business, in performance). It's about digging into the challenging problems and finding answers (and of course - having fun).
Think about why we split the input array into two equals parts? There is a nice question in Skiena's algorithms book: what would be with time complexity and algorithm itself if we split the array in two parts: 1/3 and 2/3. The best answer is for sure: "Dr. Skiena, are you simply stupid asking these questions? Just rewrite it with iterations instead and relax."
Saying "double-pivot binary search" is kinda like saying "three-wheeled bicycle".
Just to add, there can also be commercial reasons for a tricycle. In my neighborhood growing up in Miami, there was an ice cream vendor and a knife grinder who went around on trikes. In the latter case, the chain was switched over to the grinding stone to provide power.
In this case, I don't know.. Unlike in Quicksort, there is no huge cost after selecting the pivot depending on it's value.
I guess it could be useful if you could expect you're searching for items with a different random distributions than are the items in the list, e.g. you search for uniformly distributed elements in a list with non-uniform distribution (like a highly skewed one).
Compiled with maximum optimizations, the binarysearch() was 33% faster over the more complicated dualPivotBinarysearch(). I tested every element of the array, with array sizes from 100000 to 10000000 elements in steps of 100000.
An different iterative version of binary search was 6.5% faster than binarysearch() using the same test.
These would be my follow up questions.
You don't even need a stack. Binary search is an iterative process:
int binarysearch(int a[], int k, int lo, int hi) {
while (lo < hi) {
int p = lo + (hi - lo) / 2;
if (k < a[p]) {
hi = p;
} else if (k == a[p]) {
return p;
} else if (k > a[p]) {
lo = p + 1;
}
}
return -1;
}I haven't done assembly language in a couple of decades, but it seems to me that the cost of calculating the traditional pivot point will be rather cheaper than that for the dual pivots.
At least back in the day, a division by two was a trivial operation (arithmetic shift right by 1), whereas the division by three would require an actual calculation: not a big deal, but more expensive than the ASR.
More complicated method: http://www.hackersdelight.org/divcMore.pdf
But the compiler will( should, look at generated code ) optimize the constant division anyway.
---
Behold, division by three using only addition and shifting( works up to 32767 ):
unsigned int div3upto32767( unsigned int n )
{
return ( ( n << 13 )+( n << 11 )+( n << 9 )+( n << 7 )+( n << 5 )+( n << 3 )+( n << 1 )+n ) >> 15 ;
}
---This one works up to 32767, and then produces a wrong result every ~32767 numbers or so. The result is of by one. When you get over a million, every number is of by a couple of digits.
uint64_t div3almost( uint64_t n )
{
n -= ( n >> 15 ) ;
return ( ( n << 13 )+( n << 11 )+( n << 9 )+( n << 7 )+( n << 5 )+( n << 3 )+( n << 1 )+n ) >> 15 ;
}
I don't think the additions are worth it.On the other hand, a comparison is a dirty cheap operation, even using bit masks and shifts I doubt it will have a big performance impact, unless we're dealing with very very big arrays.
In my humble opinion main difference between quicksort and binary search is that one sorts numbers quickly, second is efficient in chasing down pointers. Ideally you want to avoid chasing pointers down at all, thus hashtable.
If you have two comparisons per call to your quicksort recursive function instead of just one comparison, are you reducing the complexity at all?
The benefit of this method is less recursive calls, therefore, less overhead of recursion.
2 compares with dual-pivot reduces your search space by 2/3
2 compares with single pivot reduces your search space by 3/4
Furthermore binary search can be done iteratively unlike quicksort, so reducing recursion overhead is useless.