Geralmente, uma função simples de hash funciona pegando as "partes componentes" da entrada (caracteres no caso de uma string) e multiplicando-as pelos poderes de alguma constante e adicionando-as em algum tipo inteiro. Portanto, por exemplo, um hash típico (embora não especialmente bom) de uma sequência pode ser:
(first char) + k * (second char) + k^2 * (third char) + ...
Então, se um punhado de strings, todas com o mesmo primeiro caractere, forem alimentadas, os resultados serão todos do mesmo módulo k, pelo menos até o tipo inteiro exceder.
[Como exemplo, o hashCode da string de Java é estranhamente semelhante a isso - ele faz a ordem inversa dos caracteres, com k = 31. Portanto, você obtém relações de módulo 31 entre seqüências que terminam da mesma maneira e relações de módulo 2 ^ 32 entre seqüências que são iguais, exceto no final. Isso não atrapalha seriamente o comportamento de hashtable.]
Uma hashtable funciona assumindo o módulo do hash sobre o número de buckets.
Em uma hashtable, é importante não produzir colisões para casos prováveis, pois as colisões reduzem a eficiência da hashtable.
Agora, suponha que alguém coloque um monte de valores em uma hashtable que tenha algum relacionamento entre os itens, como todos tendo o mesmo primeiro caractere. Esse é um padrão de uso bastante previsível, eu diria, portanto não queremos que ele produza muitas colisões.
Acontece que "devido à natureza da matemática", se a constante usada no hash e o número de buckets forem coprimes , as colisões serão minimizadas em alguns casos comuns. Se eles não são coprime, existem alguns relacionamentos bastante simples entre entradas para as quais as colisões não são minimizadas. Todos os hashes saem iguais ao fator comum, o que significa que todos caem no 1/1 dos baldes que têm esse valor modulado no fator comum. Você recebe n vezes mais colisões, onde n é o fator comum. Como n é pelo menos 2, eu diria que é inaceitável que um caso de uso bastante simples gere pelo menos o dobro de colisões do que o normal. Se algum usuário dividir nossa distribuição em buckets, queremos que seja um acidente estranho, não um uso previsível simples.
Agora, implementações de hashtable obviamente não têm controle sobre os itens colocados nelas. Eles não podem impedir que eles sejam relacionados. Portanto, a coisa a fazer é garantir que a constante e a contagem do balde sejam coprime. Dessa forma, você não depende apenas do "último" componente para determinar o módulo do bucket em relação a algum pequeno fator comum. Até onde eu sei, eles não precisam ser os melhores para conseguir isso, apenas coprime.
Mas se a função hash e a hashtable forem gravadas independentemente, a hashtable não saberá como funciona a função hash. Pode estar usando uma constante com pequenos fatores. Se você tiver sorte, pode funcionar de maneira completamente diferente e não linear. Se o hash for bom o suficiente, qualquer contagem de buckets será boa. Mas uma hashtable paranóica não pode assumir uma boa função de hash, portanto, use um número primo de buckets. Da mesma forma, uma função hash paranóica deve usar uma constante prime grande, para reduzir a chance de alguém usar vários buckets que, por acaso, possuem um fator comum com a constante.
Na prática, acho bastante normal usar uma potência de 2 como o número de baldes. Isso é conveniente e evita a necessidade de procurar ou pré-selecionar um número primo da magnitude certa. Portanto, você depende da função hash para não usar multiplicadores, o que geralmente é uma suposição segura. Mas você ainda pode obter comportamentos ocasionais de hash ruim com base em funções de hash como a acima, e a contagem principal de buckets pode ajudar ainda mais.
Adotar o princípio de que "tudo tem que ser primordial" é, tanto quanto eu conheço, uma condição suficiente, mas não necessária, para uma boa distribuição das hashtables. Ele permite que todos interoperem sem precisar assumir que os outros seguiram a mesma regra.
[Editar: há outro motivo mais especializado para usar um número primo de caçambas, ou seja, se você lidar com colisões com sondagem linear. Em seguida, você calcula um passo a partir do código de hash e, se esse passo for um fator da contagem de buckets, você só poderá fazer análises (bucket_count / stride) antes de voltar para onde começou. O caso que você mais deseja evitar é stride = 0, é claro, que deve ter uma caixa especial, mas para evitar também uma caixa especial bucket_count / stride igual a um número inteiro pequeno, você pode simplesmente fazer o bucket_count ser primo e não se importar com o que passo é fornecido, não é 0.]