Números primos em um determinado intervalo


10

Recentemente, recebi a tarefa de imprimir todos os números primos (1 a 100). Eu falhei drasticamente lá. Meu código:

Create Procedure PrintPrimeNumbers
@startnum int,
@endnum int
AS 
BEGIN
Declare @a INT;
Declare @i INT = 1
(
Select a = @startnum / 2;
WHILE @i<@a
BEGIN
@startnum%(@a-@i)
i=i+1;
)
END

Embora eu tenha terminado sem concluí-lo, pergunto-me se é possível executar esses programas no Banco de Dados (SQL Server 2008 R2).

Se sim, como isso pode acabar.


2
Para não tirar nenhuma das respostas dadas, mas este é o melhor artigo que eu já vi sobre o assunto: sqlblog.com/blogs/hugo_kornelis/archive/2006/09/23/…
Erik Darling

O objetivo é fazer apenas 1 a 100 ou qualquer intervalo e 1 a 100 era apenas um exemplo?
Solomon Rutzky

Na minha pergunta, era de 1 a 100. Seria bom ter uma abordagem generalista, depois uma específica.
Ispostback

Respostas:


11

De longe, a maneira mais rápida e fácil de imprimir "todos os números primos (1-100)" é abraçar completamente o fato de que os números primos são um conjunto de valores conhecido, finito e imutável ("conhecido" e "finito" dentro de um faixa específica, é claro). Nesta pequena escala, por que desperdiçar CPU a cada vez para calcular um monte de valores conhecidos há muito tempo e ocupar quase nenhuma memória para armazenar?

SELECT tmp.[Prime]
FROM   (VALUES (2), (3), (5), (7), (11), (13),
        (17), (19), (23), (29), (31), (37), (41),
        (43), (47), (53), (59), (61), (67), (71),
        (73), (79), (83), (89), (97)) tmp(Prime)

Obviamente, se você precisar calcular os números primos entre 1 e 100, o seguinte é bastante eficiente:

;WITH base AS
(
    SELECT tmp.dummy, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS [num]
    FROM   (VALUES (0), (0), (0), (0), (0), (0), (0)) tmp(dummy)
), nums AS
(
    SELECT  (ROW_NUMBER() OVER (ORDER BY (SELECT 1)) * 2) + 1 AS [num]
    FROM        base b1
    CROSS JOIN  base b2
), divs AS
(
    SELECT  [num]
    FROM        base b3
    WHERE   b3.[num] > 4
    AND     b3.[num] % 2 <> 0
    AND     b3.[num] % 3 <> 0
)
SELECT  given.[num] AS [Prime]
FROM        (VALUES (2), (3)) given(num)
UNION ALL
SELECT  n.[num] AS [Prime]
FROM        nums n
WHERE   n.[num] % 3 <> 0
AND     NOT EXISTS (SELECT *
                    FROM divs d
                    WHERE d.[num] <> n.[num]
                    AND n.[num] % d.[num] = 0
                    );

Esta consulta apenas testa números ímpares, pois os números pares não serão primos de qualquer maneira. Também é específico para o intervalo de 1 a 100.

Agora, se você precisar de um intervalo dinâmico (semelhante ao mostrado no código de exemplo da pergunta), a seguir é apresentada uma adaptação da consulta acima que ainda é bastante eficiente (calculou o intervalo de 1 - 100.000 - 9592 entradas - em menos de 1 segundo):

DECLARE  @RangeStart INT = 1,
         @RangeEnd INT = 100000;
DECLARE  @HowMany INT = CEILING((@RangeEnd - @RangeStart + 1) / 2.0);

;WITH frst AS
(
    SELECT  tmp.thing1
    FROM        (VALUES (0), (0), (0), (0), (0), (0), (0), (0), (0), (0)) tmp(thing1)
), scnd AS
(
    SELECT  0 AS [thing2]
    FROM        frst t1
    CROSS JOIN frst t2
    CROSS JOIN frst t3
), base AS
(
    SELECT  TOP( CONVERT( INT, CEILING(SQRT(@RangeEnd)) ) )
            ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS [num]
    FROM        scnd s1
    CROSS JOIN  scnd s2
), nums AS
(
    SELECT  TOP (@HowMany)
            (ROW_NUMBER() OVER (ORDER BY (SELECT 1)) * 2) + 
                (@RangeStart - 1 - (@RangeStart%2)) AS [num]
    FROM        base b1
    CROSS JOIN  base b2
), divs AS
(
    SELECT  [num]
    FROM        base b3
    WHERE   b3.[num] > 4
    AND     b3.[num] % 2 <> 0
    AND     b3.[num] % 3 <> 0
)
SELECT  given.[num] AS [Prime]
FROM        (VALUES (2), (3)) given(num)
WHERE   given.[num] >= @RangeStart
UNION ALL
SELECT  n.[num] AS [Prime]
FROM        nums n
WHERE   n.[num] BETWEEN 5 AND @RangeEnd
AND     n.[num] % 3 <> 0
AND     NOT EXISTS (SELECT *
                    FROM divs d
                    WHERE d.[num] <> n.[num]
                    AND n.[num] % d.[num] = 0
                    );

Meu teste (usando SET STATISTICS TIME, IO ON;) mostra que esta consulta tem um desempenho melhor do que as outras duas respostas fornecidas (até agora):

GAMA: 1-100

Query      Logical Reads       CPU Milliseconds    Elapsed Milliseconds
-------    ----------------    ----------------    -----------------

Solomon      0                 0                   0
Dan        396                 0                   0
Martin     394                 0                   1

GAMA: 1 - 10.000

Query      Logical Reads       CPU Milliseconds    Elapsed Milliseconds
-------    ----------------    ----------------    -----------------

Solomon        0                   47                170
Dan        77015                 2547               2559
Martin       n/a

GAMA: 1 - 100.000

Query      Logical Reads       CPU Milliseconds    Elapsed Milliseconds
-------    ----------------    ----------------    -----------------

Solomon            0                 984                996
Dan        3,365,469             195,766            196,650
Martin           n/a

GAMA: 99.900 - 100.000

NOTA : Para executar este teste, eu tive que corrigir um erro no código de Dan - @startnumnão era fatorado na consulta, portanto, sempre começava às 1. Troquei a Dividend.num <= @endnumlinha por Dividend.num BETWEEN @startnum AND @endnum.

Query      Logical Reads       CPU Milliseconds    Elapsed Milliseconds
-------    ----------------    ----------------    -----------------

Solomon       0                   0                   1
Dan           0                 157                 158
Martin      n/a

GAMA: 1 - 100.000 (reteste parcial)

Depois de corrigir a consulta de Dan para o teste 99.900 - 100.000, notei que não havia mais leituras lógicas listadas. Portanto, retestei esse intervalo com essa correção ainda aplicada e descobri que as leituras lógicas haviam desaparecido novamente e os tempos eram um pouco melhores (e sim, o mesmo número de linhas foi retornado).

Query      Logical Reads       CPU Milliseconds    Elapsed Milliseconds
-------    ----------------    ----------------    -----------------

Dan                0             179,594            180,096

Qual é o propósito ROW_NUMBER() OVER (ORDER BY (SELECT 1))? Não ROW_NUMBER() OVER ()seria equivalente?
Lennart

Oi @Lennart .Se você tentar usar OVER (), você receberá o seguinte erro: The function 'ROW_NUMBER' must have an OVER clause with ORDER BY.. E, com ORDER BY, não pode ser uma constante, daí a subconsulta retornar uma constante.
Solomon Rutzky

11
Obrigado, eu não estava ciente dessa limitação no servidor sql. Faz sentido agora
Lennart

Por que se eu usá- DECLARE @RangeStart INT = 999900, @RangeEnd INT = 1000000;lo funciona, mas assim que definido DECLARE @RangeStart INT = 9999999900, @RangeEnd INT = 10000000000;, diz Msg 8115, Level 16, State 2, Line 1 Arithmetic overflow error converting expression to data type int. Msg 1014, Level 15, State 1, Line 5 A TOP or FETCH clause contains an invalid value.?
Francesco Mantovani

11
@FrancescoMantovani Esse erro está dizendo que seus valores estão fora da faixa de INT. O valor máximo que INTpode reter é 2.147.483.647, que é menor que o valor inicial de 9.999.999.900. Você recebe esse erro mesmo se executar apenas o DECLARE. Você pode tentar alterar os tipos de dados variáveis BIGINTe ver como isso acontece. É possível que outras pequenas alterações sejam necessárias para apoiar isso. Para intervalos de tipos de dados, consulte: int, bigint, smallint e tinyint .
Solomon Rutzky

7

Uma maneira simples, mas não muito eficiente, de retornar os números primos no intervalo de 2 a 100 (1 não é primo) seria

WITH Ten AS (SELECT * FROM (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) V(N)),
     Hundred(N) AS (SELECT T1.N * 10 + T2.N + 1 FROM Ten T1, Ten T2)
SELECT H1.N
FROM   Hundred H1
WHERE  H1.N > 1
       AND NOT EXISTS(SELECT *
                      FROM   Hundred H2
                      WHERE  H2.N > 1
                             AND H1.N > H2.N
                             AND H1.N % H2.N = 0);

Você também pode potencialmente materializar os números 2-100 em uma tabela e implementar a Peneira de Eratóstenes por meio de atualizações ou exclusões repetidas.


4

Gostaria de saber se é possível fazer esses programas no banco de dados

Sim, é viável, mas não acho que o T-SQL seja a ferramenta certa para o trabalho. Abaixo está um exemplo de uma abordagem baseada em conjunto no T-SQL para esse problema.

CREATE PROC dbo.PrintPrimeNumbers
    @startnum int,
    @endnum int
AS 
WITH 
     t4 AS (SELECT n FROM (VALUES(0),(0),(0),(0)) t(n))
    ,t256 AS (SELECT 0 AS n FROM t4 AS a CROSS JOIN t4 AS b CROSS JOIN t4 AS c CROSS JOIN t4 AS d)
    ,t16M AS (SELECT ROW_NUMBER() OVER (ORDER BY (a.n)) AS num FROM t256 AS a CROSS JOIN t256 AS b CROSS JOIN t256 AS c)
SELECT num
FROM t16M AS Dividend
WHERE
    Dividend.num <= @endnum
    AND NOT EXISTS(
        SELECT 1
        FROM t16M AS Divisor
        WHERE
            Divisor.num <= @endnum
            AND Divisor.num BETWEEN 2 AND SQRT(Dividend.num)
            AND Dividend.num % Divisor.num = 0
            AND Dividend.num <= @endnum
    );
GO
EXEC dbo.PrintPrimeNumbers 1, 100;
GO

0

Podemos escrever o código abaixo e ele funciona:

CREATE procedure sp_PrimeNumber(@number int)
as 
begin
declare @i int
declare @j int
declare @isPrime int
set @isPrime=1
set @i=2
set @j=2
while(@i<=@number)
begin
    while(@j<=@number)
    begin
        if((@i<>@j) and (@i%@j=0))
        begin
            set @isPrime=0
            break
        end
        else
        begin
            set @j=@j+1
        end
    end
    if(@isPrime=1)
    begin
        SELECT @i
    end
    set @isPrime=1
    set @i=@i+1
    set @j=2
end
end

Acima, criei um Procedimento armazenado para obter números primos.

Para conhecer os resultados, execute o procedimento armazenado:

EXECUTE sp_PrimeNumber 100

0
DECLARE @UpperLimit INT, @LowerLimit INT

SET @UpperLimit = 500
SET @LowerLimit = 100

DECLARE @N INT, @P INT
DECLARE @Numbers TABLE (Number INT NULL)
DECLARE @Composite TABLE (Number INT NULL)

SET @P = @UpperLimit

IF (@LowerLimit > @UpperLimit OR @UpperLimit < 0 OR @LowerLimit < 0 )
    BEGIN
        PRINT 'Incorrect Range'
    END 
ELSE
    BEGIN
        WHILE @P > @LowerLimit
            BEGIN
                INSERT INTO @Numbers(Number) VALUES (@P)
                SET @N = 2
                WHILE @N <= @UpperLimit/2
                    BEGIN
                        IF ((@P%@N = 0 AND @P <> @N) OR (@P IN (0, 1)))
                            BEGIN
                                INSERT INTO @Composite(Number) VALUES (@P)
                                BREAK
                            END
                        SET @N = @N + 1
                    END
                SET @P = @P - 1
            END
        SELECT Number FROM @Numbers
        WHERE Number NOT IN (SELECT Number FROM @Composite)
        ORDER BY Number
        END
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.