quollcucumber1's blog

By quollcucumber1, history, 8 months ago, In English

Segment tree sorting

(If you aren't familiar with what a segment tree is then I would recommend reading https://cp-algorithms.com/data_structures/segment_tree.html )

So I came up with a cool way of sorting an array with a segment tree. I'm not sure if this has been thought of before, but if it hasn't, then it would be cool to share. I did some comparisons against other ways of sorting, and it outperformed the other ways, aside from std::sort. My implementation of the other sorting techniques could probably be improved though. So here is the general idea:

General idea

Each node of the segment tree stores two elements. It stores the minimum value of all nodes beneath it, as well as storing the index of this minimum node. Start by creating the segment tree with these elements in it, then we repeat N times : 1. Add node 1's minimum value to our sorted list. 2. Update the index of that minimum value to INT_MAX, as after adding it to the sorted array, you want to effectively remove it from the tree.

Time complexity: O(n log(n))

Implementation details

We start by constructing the array with this:

void construct(vector<int> *v) {
        for(int i = 0; i < n; i++) {
            seg[i + n] = {(*v)[i], i};
        }
        for(int i = n- 1; i> 0; i--) {
            if(seg[i * 2 + 1].first < seg[i * 2].first) {
                seg[i] = seg[i * 2 + 1];
            }else {
                seg[i] = seg[i * 2];
            }
        }
    }
}

Then for i in range(n), we find the minimum value, seg[1].first, and then set seg[1].second to INT_MAX with:

void update(int index) {
        seg[index + n].first = INT_MAX;
        for(int i = (index + n)/2; i> 0; i/=2) {
            pair<int, int> a = seg[i *2 + 1];
            pair<int, int> b = seg[i * 2];
            if(a.first < b.first) {
                seg[i] = a;
            }else {
                seg[i] = b;
            }
        }
    }
}

How did it compare to other sorting algorithms?

When I ran it on my computer for an array of size 10,000,000:

  • Standard sort took 2.09876 seconds
  • Segment tree sort took 3.10657 seconds
  • Heap sort took 4.50604 seconds
  • Merge sort took 4.61425 seconds
  • Quick sort took 5.76152 seconds

Array size of 1,000,000:

  • Standard sort took 0.177153 seconds
  • Segment tree sort took 0.192566 seconds
  • Heap sort took 0.372507 seconds
  • Merge sort took 0.407037 seconds
  • Quick sort took 0.529488 seconds

Array of size 100,000:

  • Standard sort took 0.0153477 seconds
  • Segment tree sort took 0.0158673 seconds
  • Heap sort took 0.0318967 seconds
  • Merge sort took 0.0390352 seconds
  • Quick sort took 0.0490728 seconds

Array of size 10,000:

  • Standard sort took 0.00122976 seconds
  • Segment tree sort took 0.00126829 seconds
  • Heap sort took 0.00270543 seconds
  • Merge sort took 0.00367626 seconds
  • Quick sort took 0.00454401 seconds

Even though my merge and quick sort could probably be optimized, the segment tree sort did very well, being comparable to std::sort.

Github link: https://github.com/quollcucumber/segtreesort

  • Vote: I like it
  • +28
  • Vote: I do not like it

»
8 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

Auto comment: topic has been updated by quollcucumber1 (previous revision, new revision, compare).

»
8 months ago, hide # |
 
Vote: I like it +1 Vote: I do not like it

How is your heap sort running faster than merge sort and merge sort running than quick sort?

»
8 months ago, hide # |
Rev. 2  
Vote: I like it +1 Vote: I do not like it

instead of making two arrays $$$a$$$ and $$$b$$$ for merge sort, make only one:

Code

i tested it on my computer and run it on custom test in codeforces, it will run faster on high $$$n$$$ (i tested on $$$n = 10^7, n = 7×10^6, n = 5×10^6$$$)

  • »
    »
    8 months ago, hide # ^ |
     
    Vote: I like it 0 Vote: I do not like it

    Cool! How long did the sort take compared so segment tree sort? Can you post the times? Thanks you.

    • »
      »
      »
      8 months ago, hide # ^ |
      Rev. 3  
      Vote: I like it 0 Vote: I do not like it

      only ran Segment tree sort and Merge sort here is the result ($$$n = 10^7$$$ with no O3):

      Segment tree sort took 3.74028 seconds

      Merge sort took 2.46159 seconds

»
8 months ago, hide # |
Rev. 2  
Vote: I like it +1 Vote: I do not like it

I don't think any of the other implementations are necessarily optimized (e.g. heapsort needing O(n log n) time to construct the heap) but for a small demonstration it doesn't really matter. You could check out the similar Tournament Tree

»
8 months ago, hide # |
Rev. 2  
Vote: I like it +1 Vote: I do not like it

Update: When I compiled this again, but with O3 optimization, the segment tree performed significantly worse compared to the other ways of sorting.

This was the results when compiled for 10,000,000 elements with O3:

  • Standard sort took 0.659301 seconds
  • Segment tree sort took 1.92628 seconds
  • Heap sort took 0.990761 seconds
  • Merge sort took 0.533974 seconds
  • Quick sort took 1.26678 seconds

Which is very different from the results we saw before. So it might not be the most useful method of sorting, but it is a cool idea in theory.

»
8 months ago, hide # |
 
Vote: I like it +6 Vote: I do not like it

cool, but is this not just selection sort sped up to nlogn by using segtree rmq?

»
2 months ago, hide # |
 
Vote: I like it +1 Vote: I do not like it

damn you must really love segtrees

»
2 months ago, hide # |
 
Vote: I like it +1 Vote: I do not like it

Try using bitwise shift operations for multiplication/division with 2 instead of normal operators. (ie. >> 1 for /2, << 1 for *2). These do not affect performance if used only few time, but at complexity of O(n logn) it will definately make an impact for larger n. Reason is that * and / operators (specially division) uses more CPU instructions at runtime than that of bitwise shift operations. Give it a try.

  • »
    »
    2 months ago, hide # ^ |
    Rev. 2  
    Vote: I like it 0 Vote: I do not like it

    Hi, Thanks for this recommendation, I implemented it, and these were my times (without pragma optimizations):

    Standard sort took 2.06599 seconds

    Segment tree sort took 3.08233 seconds

    Heap sort took 4.79794 seconds

    Merge sort took 4.5813 seconds

    Quick sort took 5.77187 seconds

    As you can see, it saves a small amount of time, but not a huge difference.

    When compiled with optimizations, it barely changes because I believe the compiler already optimizes *2 into <<1.

    Thanks for your recommendation though, and more are welcome anytime.

»
2 months ago, hide # |
 
Vote: I like it +1 Vote: I do not like it

Maybe it is because the updates are simple comparisons which the CPU can speculatively execute well. The memory access is also friendly as it is continuous. Recursion-based algorithms are worse here.

  • »
    »
    2 months ago, hide # ^ |
     
    Vote: I like it 0 Vote: I do not like it

    Yeah, recursive algorithms are usually quite a bit slower. I usually use recursive segment tree, but I did everything iteratively for speed for this case.