Nombres premiers dans une plage donnée

10

Récemment, on m'a confié la tâche d'imprimer tous les nombres premiers (1-100). J'ai échoué radicalement là-bas. Mon code:

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

Bien que je me sois retrouvé sans le terminer, je me demande s'il est possible de faire de tels programmes sur la base de données (SQL Server 2008 R2).

Si oui, comment cela peut se terminer.

ispostback
la source
2
Je ne retire aucune des réponses données, mais c'est le meilleur article que j'ai vu sur le sujet: sqlblog.com/blogs/hugo_kornelis/archive/2006/09/23/…
Erik Darling
Le but est-il de faire seulement 1 - 100, ou n'importe quelle plage et 1 - 100 n'était qu'un exemple de plage?
Solomon Rutzky
Dans ma question, c'était de 1 à 100. Je serais bien d'avoir une approche généraliste, puis une approche spécifique.
ispostback

Réponses:

11

La manière de loin la plus rapide et la plus simple d’ imprimer «tous les nombres premiers (1-100)» consiste à adopter pleinement le fait que les nombres premiers sont un ensemble de valeurs connues, finies et immuables («connues» et «finies» au sein d’une gamme particulière, bien sûr). À cette petite échelle, pourquoi gaspiller le CPU à chaque fois pour calculer un tas de valeurs connues depuis très longtemps et prendre peu de mémoire pour stocker?

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)

Bien sûr, si vous devez calculer les nombres premiers entre 1 et 100, ce qui suit est assez efficace:

;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
                    );

Cette requête ne teste que les nombres impairs car les nombres pairs ne seront de toute façon pas premiers. Il est également spécifique à la plage de 1 à 100.

Maintenant, si vous avez besoin d'une plage dynamique (similaire à ce qui est montré dans l'exemple de code dans la question), alors ce qui suit est une adaptation de la requête ci-dessus qui est encore assez efficace (elle a calculé la plage de 1 - 100 000 - 9592 entrées - en un peu moins d'une seconde):

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
                    );

Mes tests (en utilisant SET STATISTICS TIME, IO ON;) montrent que cette requête fonctionne mieux que les deux autres réponses données (jusqu'à présent):

GAMME: 1 - 100

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

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

GAMME: 1-10 000

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

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

GAMME: 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

GAMME: 99 900 - 100 000

REMARQUE : Afin d'exécuter ce test, j'ai dû corriger un bogue dans le code de Dan - @startnumn'était pas pris en compte dans la requête, donc il a toujours commencé à 1. J'ai remplacé la Dividend.num <= @endnumligne par Dividend.num BETWEEN @startnum AND @endnum.

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

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

GAMME: 1 - 100 000 (re-test partiel)

Après avoir corrigé la requête de Dan pour le test 99,900 - 100,000, j'ai remarqué qu'il n'y avait plus de lectures logiques répertoriées. J'ai donc retesté cette plage avec ce correctif toujours appliqué et j'ai constaté que les lectures logiques avaient à nouveau disparu et que les temps étaient légèrement meilleurs (et oui, le même nombre de lignes a été renvoyé).

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

Dan                0             179,594            180,096
Solomon Rutzky
la source
Quel est le but de ROW_NUMBER() OVER (ORDER BY (SELECT 1))? Ne serait pas ROW_NUMBER() OVER ()équivalent?
Lennart
Salut @Lennart .Si vous essayez d'utiliser OVER (), vous obtiendrez l'erreur suivante: The function 'ROW_NUMBER' must have an OVER clause with ORDER BY.. Et, avec ORDER BY, il ne peut pas être une constante, d'où la sous-requête pour renvoyer une constante.
Solomon Rutzky
1
Merci, je n'étais pas au courant de cette limitation dans le serveur SQL. Ça a du sens maintenant
Lennart
Pourquoi si je l'utilise DECLARE @RangeStart INT = 999900, @RangeEnd INT = 1000000;ça marche mais dès que je l'ai mis DECLARE @RangeStart INT = 9999999900, @RangeEnd INT = 10000000000;c'est dit 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
1
@FrancescoMantovani Cette erreur signifie que vos valeurs sont en dehors de la plage de INT. La valeur maximale INTpouvant être maintenue est de 2147483647, ce qui est inférieur à votre valeur de départ de 9999 999 900. Vous obtenez cette erreur même si vous exécutez uniquement le DECLARE. Vous pouvez essayer de changer les types de données variables BIGINTet voir comment cela se passe. Il est possible que d'autres modifications mineures soient nécessaires pour soutenir cela. Pour les plages de types de données, veuillez consulter: int, bigint, smallint et tinyint .
Solomon Rutzky
7

Un moyen simple mais pas très efficace de renvoyer les nombres premiers compris entre 2 et 100 (1 n'est pas premier) serait

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);

Vous pouvez également potentiellement matérialiser les nombres 2-100 dans un tableau et implémenter le tamis d'Ératosthène via des mises à jour ou des suppressions répétées.

Martin Smith
la source
4

Je me demande s'il est possible de faire de tels programmes sur la base de données

Oui, c'est faisable mais je ne pense pas que T-SQL soit le bon outil pour le travail. Voici un exemple d'une approche basée sur des ensembles dans T-SQL pour ce problème.

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
Dan Guzman
la source
0

Nous pouvons écrire le code ci-dessous et cela fonctionne:

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

Ci-dessus, j'ai créé une procédure stockée pour obtenir des nombres premiers.

Pour connaître les résultats, exécutez la procédure stockée:

EXECUTE sp_PrimeNumber 100
user156742
la source
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
Manoj
la source