Note that many software implementations of trees in general, when manipulated in RAM, are a lot less efficient than expected from the algorithms due to accesses all over the place that lead to many cache misses. So in that respect, the choice of algorithm and data structures are key. It's not just a problem with RAM either, any medium that can be accessed more efficiently as consecutive blocks (hard drives being also that way for instance) are concerned.
struct hash_entry {
struct hash_entry *next;
size_t hash;
/* Data */
};
struct hash_table {
size_t size; /* Number of entries in entry pointer array */
size_t used; /* Number of entries in the table */
struct hash_entry **entry;
/* pthread_rwlock_t rwlock; for thread-safe operation */
};
This is definitely not the most efficient way to go about it. Pointer chaining trades some efficiency for robustness: if you have an entry, insert never fails. Including the hash value itself in each node trades memory for ease of use, and reduces the number of full comparisons needed when a pointer chain grows long.many software implementations of trees in general, when manipulated in RAM, are a lot less efficient than expected from the algorithms due to accesses all over the place that lead to many cache misses.
A valuable lesson about modern CPUs is, computation is almost for free.
This was the era of spinning disk hard drives, and I/O speeds were much slower than they are today with SSD drives and multi-gigabyte RAM sizes (making many workloads completely cacheable in RAM). Reading the input was the clear bottleneck. If you first read the input to memory, then sort them, then you wait for all I/O to complete before you start computation (sorting). If you read the lines into a sorting data structure like a tree, you essentially do the computation while I/O is still underway; and your sort is basically complete, when I/O completes. This means that the read-then-sort takes much longer, using real-world wall-clock measurement, than reading the lines into a tree; even if the read-to-tree uses somewhat more CPU time.
Quotemany software implementations of trees in general, when manipulated in RAM, are a lot less efficient than expected from the algorithms due to accesses all over the place that lead to many cache misses.In theory, that doesn't matter, because you're only affecting K in the overall t = K * f(N) performance equation. If your N really justifies going from a log(N) algorithm to log(log(N)) (Tango Trees, say. No, wait - that's the "competitive ratio", a term I don't understand), then cache misses are going to need to be REALLY REALLY EXPENSIVE before the improvement goes away.
A valuable lesson about modern CPUs is, computation is almost for free.Unless the computation is mobile, where every unnecessary instruction drains the battery a little more.
It's all in the break-even point for N for different algorithms on a given platform.
A valuable lesson about modern CPUs is, computation is almost for free.Unless the computation is mobile, where every unnecessary instruction drains the battery a little more.
But orders of magnitude less than an unnecessary memory load, especially from actual RAM.


A large list of primes on which you want to do fast search operations is also a nice example of where you would like log(log(n)) instead of log(n). For ~ 232 entries a regular binary tree takes ~ 32 operations to do a search. If you can do that in log(log(n)) operations you go from 32 to 5. A factor of 6 in runtime can be sufficient motivation to use something a little more complex.
A large list of primes on which you want to do fast search operations is also a nice example of where you would like log(log(n)) instead of log(n). For ~ 232 entries a regular binary tree takes ~ 32 operations to do a search. If you can do that in log(log(n)) operations you go from 32 to 5. A factor of 6 in runtime can be sufficient motivation to use something a little more complex.
If you search primes within 32-bit numbers, you have only 2G numbers to test (after eliminating even numbers which are not primes). So, you only need 2G-bit lookup table - 256MBytes of memory. Today, this is not much by any means. 4GByte lookup table will give you all primes in 40-bit numbers. And this is still quite modest amount of RAM by today's standards. One lookup and you're done. That is how you get things done when you have lots of resources, not by applying older algorithms which were designed to circumvent resource scarcity.
A large list of primes on which you want to do fast search operations is also a nice example of where you would like log(log(n)) instead of log(n). For ~ 232 entries a regular binary tree takes ~ 32 operations to do a search. If you can do that in log(log(n)) operations you go from 32 to 5. A factor of 6 in runtime can be sufficient motivation to use something a little more complex.
If you search primes within 32-bit numbers, you have only 2G numbers to test (after eliminating even numbers which are not primes). So, you only need 2G-bit lookup table - 256MBytes of memory. Today, this is not much by any means. 4GByte lookup table will give you all primes in 40-bit numbers. And this is still quite modest amount of RAM by today's standards. One lookup and you're done. That is how you get things done when you have lots of resources, not by applying older algorithms which were designed to circumvent resource scarcity.
In some cases, there are also ways to circumvent the need and translate it to something else. For instance, here, you may actually not NEED to test any integer number being prime or not. It's sometimes possible to transform your approach by generating prime numbers instead of having to test them, something that's much faster than testing for primality. You sometimes need to think outside the box.
) why I try to solve toy problems and little puzzles. Not so much for the actual solution, but more as a good way to learn different ways of solving various problems. For example Project Euler has a nice collection. And after solving a particular problem you can take a look on the forum to see how other people did it. Some of it is "Yeah yeah, I did that too", or "Hah, my way is better!", but there is also plenty of "Doh! Why didn't I think of that?".
For example Project Euler has a nice collection. And after solving a particular problem you can take a look on the forum to see how other people did it. Some of it is "Yeah yeah, I did that too", or "Hah, my way is better!", but there is also plenty of "Doh! Why didn't I think of that?".
Anyways, the main point being: "4294967295 unique prime numbers", as opposed to "all prime numbers below 4294967296".
And while we're at it ... a 4 GByte LUT as described (eliminate even numbers, keep track of the rest) unfortunately only gets you up to 36-bit numbers, not 40-bit numbers.
That said, I agree that you should use the resources available, and not use old shit that is no longer relevant. Unfortunately infinity is fucking big, even if it is countable.
I just registered for some fun. Ran into some issue with the first problem though: https://projecteuler.net/problem=1
I double and triple-checked my answer, and the site still thinks it's erroneous. Could you check? Maybe I'm just tired.
Anyways, seems to works fine. Maybe just add some coffee.
[SPOILERS]
.
.
.
.
.
.
.
.
.
function summultiples(below) {
function triangular(n) { return (n*n + n) / 2; };
below--;
var threes = triangular(Math.floor(below / 3)) * 3;
var fives = triangular(Math.floor(below / 5)) * 5;
var fifteens = triangular(Math.floor(below / 15)) * 15;
return threes + fives - fifteens;
}

long summultiples(long below)
{
long i, sum=0;
for (i=3; i < below; i+=3)
sum += i;
for (i=5; i < below; i+=5)
sum += i;
for (i=15; i < below; i+=15)
sum -= i;
return sum;
}
a 4-byte access for basically every statement, that's a big stinker right there.
divisions don't matter, they are by constants so can be transformed to clever multiplications by a sufficiently clever compiler.
divisions can be transformed to clever multiplicationsThat'd be nice. It doesn't seem to happen with either the ARM or AVR gcc compilers, though.