C# Most efficient data structure to insert and to remove lower half
Asked Answered
R

4

5

Imagine I have a big sorted list of integers (>1000 items). I need to be able to do two operations on this list: remove the lower half and fill the list again to its original size by inserting random integers. Because I do these operations about a million times, I need it to be as efficient as possible.

The first thing I did was just using a List that I kept sorted by adding new items at the right place. Although removing the lower half of a sorted list is very easy, inserting takes quite a bit of time.

I tried implementing a skip list instead, but after some testing it seemed that the size of the list had to be at least 10 000 to really matter, otherwise it performed even worse than my normal list.

That's why I decided to use an AVL tree, so I could insert items much, much faster. But the problem is that I don't know any efficient way of deleting the lower half of such a binary search tree.

My question is: is there an efficient way to do this? Is there another data structure that I could use more easily?

Edit

As asked, I made a small test showing the difference in performance between a list, a skip list and an AVL tree. I made the skip list using this tutorial on msdn: Skip list tutorial. The AVL tree comes from here: AVL tree. I uploaded the test on Pastebin: Program.

In the test, I add 100 000 items to each datastructure while timing. On my pc, the list took about 1 second, the skip list 0.5 seconds and the AVL tree 0.045 seconds. If I would do this a million times like I want to, the list would take about 11.5 days, but the AVL tree would only take about half a day. This time difference clearly shows why I want it to be efficient.

Rebarbative answered 31/3, 2016 at 10:44 Comment(13)
If you use a balanced AVL tree you can just remove the left side to remove the half with the lower numbers.Catima
That's what I thought too at first, but although the tree is balanced, it doesn't mean that the left and right side of the tree have exactly the same size. For example, it's even impossible if you have a tree with an even number of items, because the root will have an odd number of children to divide in two.Rebarbative
But whatever data structure you will use you will still have the same problem with even/odd number of items. Just to make sure: you want to remove all the number that are lower than the medium value of the items, is that correct?Catima
Yes, that is correct but the odd/even size is not the only difference in size the tree can have. One of the properties of a search tree is that the height of two siblings can at most differ by 1, but if I'm not mistaken that doesn't mean the size can only differ by one. Another example: the left side of the tree can have a height of 3 with 4 leafs at the end, and the right side of the tree can have a height of 2 with only 2 leafs on the end. In total the left side has 7 children, but the right side has only 3.Rebarbative
Does it have to be removed by median value? Would it be wrong to remove all the items by the mean value? Just use a AVLtree but in the insertion method you save the mean value and update it accordingly. When you want to remove just do a tree-search and remove all values lower than median value.Catima
Removing by checking each item with the median could be a solution. Still, how can you keep track of the median value without checking all other items again? And my most important question remains, would it be efficient? For example, would it be possible/necessary to only balance the tree once instead of rebalancing after each removal?Rebarbative
To answer your question about the mean value: yes, it would be wrong to use the mean value instead of the median, because not exactly half of the tree would be deleted, which is really necessary for what I want to do.Rebarbative
@Rebarbative Three thoughts that come to mind, but they depend on your exact requirements. First is that it sounds like you're creating a sort-of (binary) heap structure... does that not solve the problem? Second, if you just use the complete trees and not partial ones, you might just fill up an array and sort it (not per value but after inserting everything). Third is that you don't need to sort everything in the second case, but only the new values and use a 2-way merge-join instead. Anyways, it all depends on your exact requirements, so please elaborate so I can think along.Kristikristian
I'd rather not download a .rar, can you put the code either in your answer or on pastebin etc? "I add 100 000 items to each data structure while timing." How many items will you be using in your application? "This time difference clearly shows why I want it to be efficient." Only if your test represents the actual workload you'll be doing. FYI I've taken multi-day workloads and turned them into seconds before, so this isn't idle chatter.Banquer
@Kristikristian I'm not really familiar with heap structures, so I'm unsure if it can solve the problem. Still, I'm now seeing that it might be possible with lists by, like you say, just sorting afterwards.Rebarbative
@Banquer I'm sorry for the rar file. I put the Program class on Pastebin and put the link in my question, the skip list and AVL tree you can find in the places I mentioned in my edit.Rebarbative
@safron it sounds like you're looking for the top m items out of n. A binary heap does exactly that in O(n log m), using a simple array and bit arithmetics as the most common implementation.Kristikristian
@Safron, I note that the benchmark code you provide does not reflect the algorithm you claim to use in the Question (e.g. removing half of items) and as such is useless for comparison. Small side note: it's better practise to break each individual test into it's own method to keep results more consistent. I whipped this up for your perusal: pastebin.com/pNgx73csBanquer
K
18

There's a few things about this question that I'd like to point out. First off, let's get a few things straight regarding performance and C# in general, because it's hard to explain stuff while there are still misconceptions.

Next, I'll apply everything I'll to the specific question here.

Performance in C# in general

Big-O notation

On university, you learn how O(n) is always better than O(n^2) and how O(n) is always better than O(n log n). However, the underlying assumption for this is that every operation will cost roughly the same amount of time.

Now, when I first started programming on an 1802 RISC processor in 1986, this was very much the case: a memory operation was 1 clock tick, and so was an add, subtract, etc. In other words, Big-O works fine there.

In a modern computer, it's more difficult:

  1. Data is cached at various levels (speeds range from 15 GB/s to 1000 GB/s);
  2. Operations vary between 0.5 clock tick and dozens of clock ticks;
  3. Data is usually fetched in bursts - so random access is much worse than sequential;
  4. Vectorization can process up to 8 integers of aligned data at once;
  5. Branch misprediction can throw everything off balance because you have to flush the lot.

I've observed that the difference in performance for different implementations of the same algorithm can be as much as a factor 1000 (!)

Big-O still holds merit though, but you should put things into perspective. For example, say that you have N=10000, then 2log N ~ 13 -- and if that means you can benefit from all these things, it might also mean that a 'stupid' O(n log n) algorithm might just outperform your average O(n) algorithm.

From this you should also deduce that an O(n^2) won't ever outperform an O(n) algorithm. So, Big-O still has its uses; you just have to put things into perspective.

Some characteristics about C#

A myth about C# is that it's approximately as fast as C++ (which is my golden standard for "as fast as it gets"). In the hands of a skilled developer it's not, simple as that. For simple sorting, C++ is approximately 2x as fast - but if you have more complicated scenario's where you can really benefit from the "low level stuff" the difference can become quite huge. I usually estimate that the difference in performance is a factor 10. However, writing proper, high performance C++ code is challenging (to use an understatement), so you might want to stick with C# and decide to take the performance hit for granted.

One thing of interest is that the C# compiler and JIT compile stuff pretty fast. In part, that's because they compile everything per-function (so, no inlining, etc). Also, C# doesn't normally vectorize stuff. Don't take my word for it, use ctrl-alt-d in Visual studio and check the assembler output for yourself.

If we look at the list above, we can roughly state that (1),(2) and (3) aren't influenced by the fact that we're using C#; (4) is definitely influenced and (5) depends.

As for (5), consider this simple example:

void set(int[] array, int index) 
{
    array[index] = 0;
}

Remember that in C# methods are compiled per-method. This means that the compiler cannot assume that index won't be out-of-bounds. In other words: it has to add two checks - and one of these will have to load memory:

if (index < 0 || index >= array.Length) 
{ 
    throw new IndexOutOfRangeException(); 
}

Sorting items

The question by the OP is about maintaining a sorted list of size m. Sorting is a well-known operation, which will cost O(log m) per item you insert at best. Since you're processing n 'random' items, you will get a best possible speed of O(n log m).

A binary heap (based on an array) might get you that performance number, but I don't feel like writing down a heap right now, and think this alternative is about the same speed (if not faster) :)

Your question

Now that we've established what we're talking about, let's just get into the facts at hand. I'll just explain this every step of the way.

First, while doing performance stuff, I make it a habit to remove using System.Linq so we know we're just working on the native data structures with the expected characteristics.

Let's start with a tree structure

Another simple solution would be to use a red-black tree. We have one at our disposal in .NET, called a SortedSet. It uses references, arithmetics, etc -- which is basically all the nasty stuff that I've warned about in (1), (2) and (3). Now, there are errors in the implementation here (for duplicates), but the speed is pretty much what you would expect:

static void Main(string[] args)
{
    Random rnd = new Random(12839);

    SortedSet<int> list = new SortedSet<int>();

    for (int i = 0; i < 5000; ++i)
    {
        list.Add(rnd.Next());
    }

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 10000; ++i)
    {
        for (int j = 0; j < 5000; ++j)
        {
            list.Add(rnd.Next());
        }
        int n = 0;
        list.RemoveWhere((a) => n++ < 5000);
    }

    Console.WriteLine(sw.ElapsedMilliseconds);

    Console.ReadLine();
}

Like practically all algorithms here, this algorithm executes in O(n log m).

What I approximately expect from AVL trees: 86220 ms.

Naive implementation

Normally I wouldn't have bothered with the red-black trees. Still, since you put a lot of work in the AVL trees, I felt it was necessary to get this measurement.

When I'm doing performance optimizations of algorithms, I always start off with the easiest-to-implement algorithm that has approximately the right Big-O and always prefer the thing that has the easiest data structure (read: array). In this case, it's a list combined with a standard sort, which will give O(m log m) for every sort, executed m/n times, and O(n) data operations. The result is O(n + n log m).

So, why go for the easiest implementation you might ask? The answer is simple: easy implementations are also easy to compile & optimize, because they usually don't have a lot of branches, don't use a lot of random memory access, etc.

The most naive implementation (that I've already suggested in my comment) is to simply put stuff in an array, sort it, and then remove the bottom half of it.

Basically this can be implemented like this in a minimum test case:

static void Main(string[] args)
{
    Random rnd = new Random(12839);
    List<int> list = new List<int>();

    for (int i = 0; i < 5000; ++i)
    {
        list.Add(rnd.Next());
    }

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 10000; ++i)
    {
        for (int j = 0; j < 5000; ++j)
        {
            list.Add(rnd.Next());
        }

        list.Sort((a, b) => a.CompareTo(b)); // #1
        list.RemoveRange(0, 5000);
    }

    Console.WriteLine(sw.ElapsedMilliseconds);
    Console.ReadLine();
}

Baseline performance: 10047 ms.

Optimization 1: remove method calls and branches

Method calls cost time. So do branches. So, if we don't need to branch, we might as well just eliminate that. In other words: this is about (5).

One thing that comes to mind is to replace #1 by:

list.Sort((a, b) => a - b);

In most (!) scenario's this gives the desired result, I bluntly assume this scenario is no exception.

Measurement: 8768 ms. (Yes people, that's a 15% change!)

For the fun of it, we also do a simple test for (2) with:

list.Sort((a, b) => (int)((float)a - (float)b));

It's exactly the same size of the operators (32 bits), it's exactly the same data and will give the same results -- we're just comparing everything with a different CPU operation and adding some casts. Measurement: 10902 ms. This is more than you would expect if every operation was just a single clock tick.

Optimization 2: Arrays or lists?

I could also care about the list itself; we're using quite a few calls to it, so we might substitute it for an array. We could even eliminate the RemoveRange if we would invert the sort order. So why don't I focus on that instead? Well, actually I could, but I can tell you it won't make a lot of difference, because relatively speaking, it's not called that often. Still, no harm in testing, right?:

static void Main(string[] args)
{
    Random rnd = new Random(12839);

    int[] list = new int[10000];

    for (int i = 0; i < 5000; ++i)
    {
        list[i] = rnd.Next();
    }

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 10000; ++i)
    {
        for (int j = 0; j < 5000; ++j)
        {
            list[j + 5000] = rnd.Next();
        }
        Array.Sort(list, (a, b) => b - a);
    }

    Console.WriteLine(sw.ElapsedMilliseconds);

    Console.ReadLine();
}

Now, there are two measurements here:

  • Changing the list to an array just changed it to +/- 8700 ms - which doesn't make much of a difference
  • Reversing the order changed the result to 7456 ms.

The reason this doesn't really make a difference, is that the underlying data structure for a List is an array, so if we're sorting, we're just working on the same thing. And that's where our time is.

The thing to remember here is not that arrays are just as fast as a List. Truth is: I've found that if they are they are actually faster in a lot of cases. However, in this case, we're not talking about optimizations in the inner loop, we're not overallocating too much memory (everything is kept in cache probably) and all memory access is aligned. All in all, we can therefore expect the difference to be quite small.

Optimization 3: remove more method calls

Now, you should notice that there's also an alternative here: method calls cost time, and the one here that's called the most here is the comparer. So let's go back to the solution with the List and remove the compare operation. When we do that, we still have to copy. What do you expect?

Change the line to this:

list.Sort();

... and we have a new timing: 4123 ms.

Now, to be entirely fair, actually what we've done here is changing our inline delegate to Comparer<int>.Default, which is the default implementation of the integer comparer. The delegate would have been wrapped in a comparer, creating 2 virtual calls - this is just 1 call. This means we could have reversed the order by implement our own comparer class as well, which would have been an even faster solution.

Optimization 4: Merge-join

Why sort everything if we only need to sort half of the data? That doesn't make sense, right?

Again, I pick the simplest possible algorithm to ackomplish the task. We walk through the list in sequential order, and store new items in sequential order, c.f. (1) and (3). No swapping, remember that we prefer sequential data access patterns. Then, we simply remove all the stuff we don't need anymore.

The algorithm we need is a merge-join, and works like this:

Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 10000; ++i)
{
    for (int j = 0; j < 5000; ++j)
    {
        list.Add(rnd.Next());
    }

    // Sort the second half:
    list.Sort(5000, 5000, Comparer<int>.Default);
            
    // Both the lower and upper half are sorted. Merge-join:
    int lhs = 0;
    int rhs = 5000;
    while (list.Count < 15000)
    {
        int l = list[lhs];
        int r = list[rhs];
        if (l < r)
        {
            list.Add(l);
            ++lhs;
        }
        else if (l > r)
        {
            list.Add(r);
            ++rhs;
        }
        else
        {
            while (list.Count < 15000 && list[lhs] == l)
            {
                list.Add(l);
                ++lhs;
            }
            while (list.Count < 15000 && list[rhs] == r)
            {
                list.Add(r);
                ++rhs;
            }
        }
    }

    list.RemoveRange(0, 10000);
}

We have a new measurement, it's 3563 ms.

Optimization 5: RemoveRange #2

Remember, processing data in burst is very fast. The last piece of code is a perfect opportunity to show this in action. We use a RemoveRange here, which processes data in burst. We can also use two buffers and swap them around. Basically we write in a second list2 during the merge-join and substitute the RemoveRange with:

list.Clear();

var tmp = list;
list = list2;
list2 = tmp;

We now have a new timing: 3542 ms. Exactly the same!

From the last two you should conclude that doing burst operations costs so little time, that you usually shouldn't even bother.

Conclusion

I've started off with a tree that executed everything in 86220 ms and ended up with an algorithm that took 3542 ms. Quite blunt this means that the last implementation executes in 4% of the time of the first attempt.

Now, I did use different algorithms throughout this long answer - but the point was to show you how to do all these optimizations and what effect optimizations really have.

Kristikristian answered 3/4, 2016 at 20:52 Comment(3)
I didn't know before I read it, but this was exactly the answer I was looking for. I'm a stunned by how such small changes can influence the performance so much. Thank you for this very informative answer!Rebarbative
I think you made a small mistake: in your merge-join you're actually throwing away the upper half instead of the lower one, aren't you?Rebarbative
@Rebarbative Yea you're right. It's not production grade; usually I create a 'test' with assertions first, and then start optimizing - which ensures all intermediate results are correct. Here I was mostly focusing on the main theory, leaving a ton of things out, with little regard for correctness of the actual results... there's just so much to explain, perhaps I should just write a book. :)Kristikristian
B
2

Why the assumption that you need a different data-structure? To say that:

The first thing I did was just using a List that I kept sorted by adding new items at the right place. Although removing the lower half of a sorted list is very easy, inserting takes quite a bit of time

Concerns me, because you may be using the correct[1] data-structure, with a poor algorithm. May I strongly suggest you take a look at http://sscce.org/ and include it in your question?

But List Insertion is Slow O(n)!

Don't insert!

As explained by @usr a far better algorithm might be something like:

  1. Remove the lower half.
  2. Add random integers to restore the original size
  3. Sort the list

No need to change the data structure, but a big change to how you solve the problem.

This is especially important because, as reiterated by @atlaste not every system is equal regardless of O(?):

Things just aren't that easy anymore with modern processors. I've seen cases where different implementations of the same algorithms give you a factor 100+ in difference due to branch prediction, vectorizing and cache locality. O(..) calculations are great if you're comparing apples with apples - but unfortunately that might not be the case. I wish everyone would just watch this video: youtube.com/watch?v=GPpD4BBtA1Y

But I still a O(log n) over a O(n) data-structure!

Okay, before we finish here and move onto actually outline the algorithm you use and measuring performance (which seems too much to ask at current) let me ask you one question:

Let's assume we have a "big sorted list of integers (>1000 items)". In fact let's say that this list is 10,000 items long!

Which of these has better insertion performance in terms of O(?)?

A) List

B) Linked List

C) Binary Tree

When you're ready, take a look at the answer:

They all have O(1)! O(n) only tells you how well things scale (relative to themselves, and only in broad terms). Since the list was of a fixed size '10,000 items' there is no scaling (everything is considered a 'constant factor'). Note, I'm not claiming that these structures are equally performant... just that O(?) has limits in its description. For more info What is a plain English explanation of "Big O" notation?

Benchmark

Here's a benchmark of insertion sort, vs sort after adding all new random items: http://pastebin.com/pNgx73cs

Results (Default Settings)

Testing performance of filling a list to 10000 items 1000 times, discarding 1/2
of items after every fill!

Old list: 3248ms
New list: 547ms
DONE

Note that even though we have a much more efficient approach in O(?) terms, the results are not that far apart because at this size CPU's are surprisingly fast!

Notes:

  1. OP has relatively small[2] collection of integers, that should easily fit inside CPU cache[3] even L1 cache. In these instance small, contiguous and predictable memory use (e.g. array and list (in C# based on an array)) can be very efficient.
  2. Assuming 10,000 as an upper bound, this would only be ~40KB or ~80KB for 32bit and 64 bit systems respectively.
  3. Intel Skylake has 64 KiB of L1 cache and 256KiB of L2 cache per core: https://en.wikipedia.org/wiki/Skylake_(microarchitecture)
Banquer answered 1/4, 2016 at 21:30 Comment(6)
The problem with using the list was that when an item is inserted, all next items have to move a place down the ladder, which can take a lot of time for big lists since it's an O(n) algorithm. With another data structure like binary search trees this can be done with an O(log n) algorithm, wich is a lot faster when we're talking about big lists. I will include an example in a moment...Rebarbative
@Rebarbative Do not confuse O(n) and O(log n) with performance. Do not confuse an data structure issue with an algorithm issue - inserting on a list is often 'slow'... so why are you inserting?Banquer
Firstly, I am inserting in the list because I want the list to keep sorted, as this allows me to remove the lower half by just calling RemoveRange(size/2,size/2) (I sort descending). Secondly, if I'm not mistaken, the O(n) and O(log n) shows how fast the performance will worsen when the size of the list/tree grows, which is especially important in this case because of the big size. When you compare a size of 100 and a size of 10000, the O(n) algorithm will perform 100 times worse whereas the O(log2 n) algorithm will only perform 2 times worse.Rebarbative
@Rebarbative Yes and no. Things just aren't that easy anymore with modern processors. I've seen cases where different implementations of the same algorithms give you a factor 100+ in difference due to branch prediction, vectorizing and cache locality. O(..) calculations are great if you're comparing apples with apples - but unfortunately that might not be the case. I wish everyone would just watch this video: youtube.com/watch?v=GPpD4BBtA1Y . That all said, I do believe that in this particular scenario the bottom line is that you're right (mostly because C# doesn't do aggressive opt).Kristikristian
@Atlaste don't underestimate C# - a simple array or list is reasonably performant, especially when considering other data structures that use pointers spread over a heap. Numbers, not guess should be driving optimization.Banquer
@Banquer I partially agree with you. Arrays are performant, no comment there. Lists... it depends on the scenario. Still, optimization is an engineering trait. I don't do random experiments for everything I can think of; I usually know what's going to happen before I do the experiment. It's not about numbers nor about guessing: Engineering means you estimate the result based on experience and facts and then test the outcome to see if it matches. For explanation, I've taken the time to write down a complete answer. Still, I guess that's what you mean as well.Kristikristian
L
2

I assume you want to keep the list sorted at all times. The best way to replace the lower half with random integers is this:

  1. Remove the lower half.
  2. Add random integers to restore the original size
  3. Sort the list

Previously, you inserted at the right position. This effectively implements a selection sort which is extremely slow. Rather, let the built-in efficient sort algorithm do the heavy work.

This should be O(n * log n). Previously it was O(n^2).

You can optimize this by a constant factor by not removing the first half. Instead, replace it with random numbers and then sort.

Lorri answered 2/4, 2016 at 21:41 Comment(3)
This is the exact algorithm I thought of when I first read the question, though until OP provide more information it is speculative.Banquer
I thought that my previous algorithm was only O(n) instead of O(n^2). Why is that? It seems that just sorting after adding all items might be the way to go, like everyone is suggesting now.Rebarbative
Each insert took N units of time and you insert N times. That's N^2 and an insertion sort (not selection as falsely stated in the answer).Lorri
D
0

The post of @atlaste is very informative, but anyway, can it be done faster? I altered the implementation a bit and got from 3750ms to 3350ms and that was my starting point. If you look at the algorithm, over time you fill half of the array with random numbers, but chances are, you would use very little of them. You can discard all numbers that are bigger than the biggest numbers in the first half right away and not bother sorting them. It would be most of the new data, so the speedup would be massive (for random input). Applying this idea I got to 640ms. Given the fact, that 470ms takes the generating of random numbers, is is about 17x speedup in the processing algorithm. But it may vary, depending on the character of the data.

and the code

public static List<int> orig()
{
    Random rnd = new Random(12839);

    List<int> list = new List<int>(10000);

    for (int i = 0; i < 5000; ++i)
    {
        list.Add(rnd.Next());
    }

    for (int i = 0; i < 10000; ++i)
    {
        for (int j = 0; j < 5000; ++j)
        {
            list.Add(rnd.Next());
        }

        // Sort the second half:
        list.Sort(5000, 5000, Comparer<int>.Default);

        // Both the lower and upper half are sorted. Merge-join:
        int lhs = 0;
        int rhs = 5000;
        while (list.Count < 15000)
        {
            int l = list[lhs];
            int r = list[rhs];
            if (l < r)
            {
                list.Add(l);
                ++lhs;
            }
            else if (l > r)
            {
                list.Add(r);
                ++rhs;
            }
            else
            {
                while (list.Count < 15000 && list[lhs] == l)
                {
                    list.Add(l);
                    ++lhs;
                }
                while (list.Count < 15000 && list[rhs] == r)
                {
                    list.Add(r);
                    ++rhs;
                }
            }
        }

        list.RemoveRange(0, 10000);
    }

    return list;
}


public static int[] altered()
{
    Random rnd = new Random(12839);

    int HALFSIZE = 5000;
    int SIZE = 2 * HALFSIZE;
    int TESTLOOPS = 10000;

    int[] list = new int[SIZE];
    int[] list2 = new int[SIZE];

    for (int i = 0; i < HALFSIZE; ++i)
    {
        list[i] = rnd.Next();
    }

    for (int i = 0; i < TESTLOOPS; ++i)
    {
        for (int j = HALFSIZE; j < list.Length; ++j)
        {
            list[j] = rnd.Next();
        }

        // Sort the second half:
        Array.Sort(list, HALFSIZE, HALFSIZE, Comparer<int>.Default);

        // Both the lower and upper half are sorted. Merge-join:
        int lhs = 0;
        int rhs = HALFSIZE;
        int i2 = 0;
        while (i2 < HALFSIZE)
        {
            int l = list[lhs];
            int r = list[rhs];
            if (l <= r)
            {
                list2[i2++] = l;
                ++lhs;
            }
            if (l >= r)
            {
                list2[i2++] = r;
                ++rhs;
            }
        }

        var tmp = list;
        list = list2;
        list2 = tmp;
    }

    return list;
}

public static int[] altered2()
{
    Random rnd = new Random(12839);

    int HALFSIZE = 5000;
    int SIZE = 2 * HALFSIZE;
    int TESTLOOPS = 10000;

    int[] list = new int[SIZE];
    int[] list2 = new int[SIZE];

    for (int i = 0; i < HALFSIZE; ++i)
    {
        list[i] = rnd.Next();
    }

    for (int i = 0; i < TESTLOOPS; ++i)
    {
        for (int j = HALFSIZE; j < list.Length; ++j)
        {
            list[j] = rnd.Next();
        }

        // quicksort one level to skip values >= maxValue
        int maxValue = list[HALFSIZE - 1];
        int ll = HALFSIZE;
        int rr = SIZE - 1;
        do
        {
            while (ll <= rr && list[ll] < maxValue) { ++ll; }
            while (ll < rr && list[rr] >= maxValue) { --rr; }
            if (ll < rr)
            {
                int swap = list[ll];
                list[ll] = list[rr];
                list[rr] = swap;
                ++ll;
                --rr;
            }
        }
        while (ll < rr);

        // Sort the second half:
        Array.Sort(list, HALFSIZE, ll - HALFSIZE, Comparer<int>.Default);

        // Both the lower and upper half are sorted. Merge-join:
        int lhs = 0;
        int rhs = HALFSIZE;
        int i2 = 0;
        while (i2 < HALFSIZE)
        {
            int l = list[lhs];
            int r = list[rhs];
            if (l <= r)
            {
                list2[i2++] = l;
                ++lhs;
            }
            if (l >= r)
            {
                list2[i2++] = r;
                ++rhs;
            }
        }

        var tmp = list;
        list = list2;
        list2 = tmp;
    }

    return list;
}

public static int[] random()
{
    Random rnd = new Random(12839);

    int HALFSIZE = 5000;
    int SIZE = 2 * HALFSIZE;
    int TESTLOOPS = 10000;

    int[] list = new int[SIZE];
    for (int i = 0; i < HALFSIZE; ++i)
    {
        list[i] = rnd.Next();
    }

    for (int i = 0; i < TESTLOOPS; ++i)
    {
        for (int j = HALFSIZE; j < list.Length; ++j)
        {
            list[j] = rnd.Next();
        }              
    }

    return list;
}

static void Main(string[] args)
{
    int HALFSIZE = 5000;
    var origTest = orig();                       
    Stopwatch sw = Stopwatch.StartNew();
    orig();
    sw.Stop();
    Console.WriteLine("Orig time: " + sw.ElapsedMilliseconds);

    var alteredTest = altered();
    sw = Stopwatch.StartNew();
    altered();
    sw.Stop();
    Console.WriteLine("Altered time: " + sw.ElapsedMilliseconds);
    Console.WriteLine("Test: " + (origTest.Take(HALFSIZE).SequenceEqual(alteredTest.Take(HALFSIZE)) ? "OK" : "BAD"));

    var altered2Test = altered2();
    sw = Stopwatch.StartNew();
    altered2();
    sw.Stop();
    Console.WriteLine("Altered2 time: " + sw.ElapsedMilliseconds);
    Console.WriteLine("Test: " + (origTest.Take(HALFSIZE).SequenceEqual(altered2Test.Take(HALFSIZE)) ? "OK" : "BAD"));

    sw = Stopwatch.StartNew();
    random();
    sw.Stop();
    Console.WriteLine("Just random time: " + sw.ElapsedMilliseconds);
    
    Console.ReadKey();
}
Discrown answered 9/5, 2021 at 18:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.