Написал программу для печати простых чисел. Как мне ее улучшить?

Я начал изучать C накануне и написал программу для печати в заданном диапазоне. Минимальный и максимальный диапазон задается пользователем.

#include <stdio.h>
int check(int number)
{
    if (number == 1)
    {
        return 0;
    }
    int count = 2;
    while (count < number)
    {
        if (number%count == 0)
        {
            return 0;
        }
        count++;
    }
    return 1;
}
void main()
{
    int i,min,max;
    printf("Enter start of range: ");
    scanf("%d",&min);
    printf("Enter end of range: ");
    scanf("%d",&max);
    for (i = min;i < max;i++)
    {
        if (check(i) == 1)
        {
            printf("%d\n",i);
        }
    }
}

Я получаю результат, как и ожидалось, но хочу знать, есть ли лучший способ вернуть 0, если введенное число равно 1 или нет, поскольку мне оно кажется кодом изоленты.

Я вставил этот блок кода до того, как программа пройдет цикл while для проверки чисел>1.

if (number == 1)
    {
        return 0;
    }

Я получаю ожидаемый результат, но хочу знать, есть ли лучший способ, чем просто разместить оператор if.

Мой вопрос отличается от предложенного, потому что я хочу знать, есть ли лучший способ выяснить, не является ли 1 простым числом. В предложенном вопросе использовался оператор if, как и я.

То, что вы написали, совершенно нормально. Его легко читать и понимать, а компилятору также легко его оптимизировать при необходимости. Единственное изменение, которое я бы сделал, — это заменить цикл в функции check на цикл for.

Some programmer dude 30.08.2024 09:42

За исключением лучшего (то есть «менее общего») имени функции (возможно, is_prime или подобного), с этой функцией все в порядке. Ваши недостатки находятся в main, где вы всегда должны проверять возвращаемое значение scanf

Gerhardh 30.08.2024 09:48

Вы можете улучшить основной цикл, начав с нечетного числа, ближайшего к min, и каждый раз увеличивая счетчик циклов на два. Проверять четные числа нет смысла, так как они (кроме 2) не могут быть простыми. Кроме того, в вашем методе check вам нужно посчитать только до (number /2), так как к этому времени все действительные деления будут завершены.

OldBoy 30.08.2024 09:59

Вам нужно проверить только sqrt (число)

pm100 30.08.2024 10:23

@pm100 введение чисел с плавающей запятой в по существу целочисленную задачу создает больше проблем, чем необходимо.

pmg 30.08.2024 10:41

Как сделать это лучше? Всегда проверяйте возврат каждой функции ввода, чтобы убедиться, что ввод выполнен успешно или неудачно.

David C. Rankin 30.08.2024 10:59

Вероятно, самое большое улучшение производительности можно было бы получить, если бы тест на деление на 2 был создан с использованием if (!(number & 1)) return 0, а затем каждый раз начинался отсчет с 3 и увеличивался на count += 2. Также измените условие цикла на while (count*count < number). И то, и другое предотвращает много ненужной работы. Немного более сложная схема с чередованием приращений 2 и 4 позволяет пропустить все числа, делящиеся на три. Общая эвристика заключается в том, что деление — это самая затратная операция: почти каждая вторая двоичная операция занимает один машинный цикл на современном процессоре.

Martin Brown 30.08.2024 12:01

@MartinBrown if (!(number & 1)) return 0 лучше как if (number % 2 == 0) return 0 для более четкого кода (и педантичной работы с дополнением, отличным от 2). if (number % 2 == 0) return number == 2 еще лучше. count*count < number переполняется для больших простых чисел. count < number/count позволяет избежать этого и требует небольших затрат для компилятора, который выполняет / и % вместе.

chux - Reinstate Monica 30.08.2024 15:18

Вы должны опубликовать это на codereview.stackexchange.com.

kiner_shah 30.08.2024 15:18

@chux Последняя машина, которую я использовал, которая не была дополнением к двум, была еще в 1984 году. Есть ли какие-либо современные процессоры, которые все еще имеют знаковую величину (возможно, чипы DSP?) или дополняют их в наши дни? Я допускаю, что существует возможность переполнения, но это может произойти только в том случае, если число является простым числом Мерсенна в форме 2^N-1, где N — размер машинного целого числа в битах. Так что я думаю, что для 32-битных целых чисел это не удастся, поскольку 2^31-1 является простым числом. В противном случае числа этой формы являются составными и обычно делятся хотя бы на одно из 3,5,7. Еще одно тривиальное улучшение программы — использование unsigned int.

Martin Brown 30.08.2024 16:28

@MartinBrown 1) Проблема с дополнением, отличным от 2, отложена в сторону. number % 2 == 0 остается более ясным, чем !(number & 1) 2) «возможность переполнения, но это может произойти только в том случае, если число является простым числом Мерсенна» -> divisor * divisor переполняется, когда number == 2147483629 или 2147483549 или другие выбранные большие простые числа, даже если не простое число Мерсенна.

chux - Reinstate Monica 30.08.2024 17:37

@chux, ты прав. Я не заметил этой подвоха.

Martin Brown 30.08.2024 17:55

Есть Code Review@SE.

greybeard 30.08.2024 18:28

Извиняюсь @greybeard, я не знал, что такое существует.

nkminion 31.08.2024 15:42

Ничего, если я явно преобразую квадратный корень в целое число и буду выполнять итерацию до этого целого числа?

nkminion 31.08.2024 15:44
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
15
243
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

Это одна из проблем, с которой сталкиваются все начинающие программисты: как проверить, является ли число простым, и обычно идея заключается в следующем:

  • Сделайте счетчик, начиная с 2, дойдите до самого числа и проверьте с помощью функции по модулю.
    (Вопрос: действительно ли вы должны заходить так далеко? Представьте, что вы проверяете 37, действительно ли ваш счетчик должен дойти до 36?)
  • Сделайте счетчик, начиная с 2, дойдите до половины самого числа и проверьте с помощью функции по модулю.
    (Опять тот же вопрос, нужно ли заходить так далеко? Представьте, что вы проверяете 391, когда у вас есть 17 и 23, у вас уже есть все из них. Так что не нужно идти выше... (не половины, а . ..номера))
  • О, вы правы: создайте счетчик, начиная с 2, дойдите до квадратного корня из самого числа и проверьте, используя функцию по модулю.

Также проверяйте только по модулю нечетные числа, кроме 2.

Fredrik 30.08.2024 10:13

@Fredrik: не стесняйтесь изменять мой ответ, он может стать ссылкой на вопросы такого типа :-)

Dominique 30.08.2024 10:14

На самом деле вам не нужно вычислять квадратный корень... просто увеличивайте число до тех пор, пока проверяемое число, умноженное само на себя, не превысит предел.

pmg 30.08.2024 10:40

Я думаю, что это на самом деле не отвечает на заданный вопрос, в котором основное внимание уделяется «хочу знать, есть ли лучший способ вернуть 0, если введенное число равно 1 или нет, как мне кажется, это код изоленты». Я думаю, что код ОП соответствует принятому определению, а ваши шаги — нет. Кроме того, меня не устраивает точность формулировки последних шагов, которую можно прочитать так, что соответствующий код имеет ошибку на единицу.

Yunnosch 30.08.2024 10:42

@pmg "идите до тех пор, пока число, которое вы проверяете, умноженное само на себя, не превысит предел". имеет недостаток. Он переполняется, когда limit является большим простым числом.

chux - Reinstate Monica 30.08.2024 15:01

Верно, @chux, спасибо. ОП, я исправляю свой комментарий на «подниматься до n > limit / n»

pmg 30.08.2024 15:12

Является ли проверка на частное быстрее, чем итерация до извлечения квадратного корня?

nkminion 31.08.2024 18:21

Более полезно, если вы хотите стать программистом на языке C... Что произойдет, если я введу «лягушку» в качестве минимального значения. Теперь вы входите в мир обнаружения и восстановления ошибок.

Немного запутанно, но я воспринимаю это как ответ на вопрос «Как улучшить этот код?». ;-)

Yunnosch 30.08.2024 10:40

«разговорчивый», возможно, означает «проверяйте каждый вводимый возврат или рискуйте UB» :)

David C. Rankin 30.08.2024 11:00

«Что произойдет, если я введу «лягушку» в качестве минимального значения?» Счетчик программы безоговорочно перейдет в ближайший пул памяти.

Lundin 30.08.2024 12:00

Я об этом не подумал, спасибо.

nkminion 31.08.2024 15:42

Проверка возврата от функций ввода важна. Также возможно решить эту проблему путем инициализации таких переменных, как min и max, при их объявлении, вместо того, чтобы рассчитывать на то, что scanf сделает это.

Chris 01.09.2024 02:33

Есть несколько способов улучшить сам алгоритм обнаружения простых чисел. Ранее на него уже давался подробный ответ, например. здесь, поэтому не буду углубляться в это.

Далее возникает вопрос о том, как проверить ввод. scanf() возвращает количество успешно преобразованных значений, поэтому его необходимо проверить, чтобы убедиться, что пользователь ввел допустимое целое число. Также следует проверить, находятся ли значения в допустимом диапазоне. Функция check() предполагает, что аргумент является положительным целым числом, поэтому либо ее необходимо сделать устойчивой к неположительному целому числу, либо гарантировать, что она будет вызываться только с положительным целым числом.

В частности, проверку можно изменить на:

    if (number <= 1)
    {
        return 0;
    }

Однако, возможно, лучше проинформировать пользователя, если введенные данные кажутся неправильными. В целом функцию main() можно записать следующим образом:

int main()
{
    int i,min,max;
    printf("Enter start of range: ");
    if (scanf("%d",&min) != 1)
    {
       printf("Input must be an integer\n");
       return 0;
    }
    if (min < 1)
    {
       printf("Input must be a positive integer\n");
       return 0;
    }
    printf("Enter end of range: ");
    if (scanf("%d",&max) != 1)
    {
       printf("Input must be an integer\n");
       return 0;
    }
    if (max < min)
    {
       printf("Range is empty\n");
       return 0;
    }
    for (i = min;i <= max;i++)
    {
        if (check(i) == 1)
        {
            printf("%d\n",i);
        }
    }
}

Примечание. В цикле for я изменил конечное условие на i <= max, поскольку пользователь, скорее всего, ожидает, что конец диапазона будет включен в проверяемые значения.

Как показывает этот пример, часто легко создать программу, которая работает, когда все идет хорошо. Создать программу, достаточно устойчивую ко всем исключениям, значительно сложнее.

Ответ принят как подходящий

Ваша программа работает так, как и ожидалось, при правильном вводе, и это хорошее начало. Вот способы улучшить код, особенно функцию check по запросу:

  • используйте правильный прототип main без аргументов: int main(void).
  • проверьте правильность ввода: scanf() возвращает 1, было ли введено целое число, в противном случае вам придется обработать ошибку явно.
  • используйте более поясняющее имя для функции check, например isprime.
  • в C идиоматично, что логические функции возвращают 0 в случае ложности и ненулевое значение в случае истины. Просто тестирование if (isprime(i)) более читабельно.
  • проверьте наличие дополнительных значений для отклонения: if (n < 2) вместо if (n == 1). Если вы не можете предположить, что аргумент n равен как минимум 2, вам нужно использовать такой тест, чтобы отклонить более низкие значения. Используя другой цикл, вы могли бы попытаться избежать этого теста, но это того не стоит: этот тест прост и удобен для чтения и почти ничего не будет стоить, если большинство значений верны.
  • проверка остатков деления должна прекратиться, как только частное станет меньше делителя.
  • явное тестирование 2 перед циклом и проверка только нечетных делителей в цикле еще больше улучшит производительность.
  • для вычисления простых чисел в диапазоне пробное деление менее эффективно, чем древнее Решето Эратосфена.

Вот модифицированная версия:

#include <stdio.h>

int isprime(int number)
{
if (number < 2) {
return 0;
}
if (number % 2 == 0) {
return number == 2;
}
for (int divisor = 3;; divisor += 2) {
// optimizing compilers will likely compute both the
// quotient and remainder with a single instruction
int quotient = number / divisor;
int remainder = number % divisor;
if (quotient < divisor)
return 1;
if (remainder == 0)
return 0;
}
}

int main(void)
{
int i, min, max;

printf("Enter start of range: ");
if (scanf("%d", &min) != 1) {
printf("invalid input\n");
return 1;
}
printf("Enter end of range: ");
if (scanf("%d", &max) != 1) {
printf("invalid input\n");
return 1;
}
for (int i = min; i < max; i++) {
if (isprime(i)) {
printf("%d\n", i);
}
}
return 0;
}

Чем «void main()» отличается от «int main(void)»?

nkminion 31.08.2024 18:25

Является ли использование оператора if единственным способом проверить исключаемые числа ниже 2?

nkminion 31.08.2024 19:37

@nkminion «Использование оператора if — единственный способ проверить...» --> Нет. Альтернатива: вернуть сравнение.

chux - Reinstate Monica 31.08.2024 20:08

Это мелочь, но, вероятно, эти "invalid input\n" сообщения будут распечатаны на stderr, а не на stdout.

Chris 01.09.2024 02:30

Помимо проверки того, является ли каждое число в диапазоне простым, есть и другие подходы:

Решето Эратосфена

Это алгоритм генерации всех простых чисел от 1 до N.

Его временная сложность O(max log log max) делает его намного быстрее. Однако для больших чисел у него есть недостатки, такие как высокие требования к пространству и перегрузка кэша. Существуют варианты алгоритма, которые решают эти проблемы, например «Сегментированное сито» и «Инкрементное сито».

Предварительно вычисленный массив

Для 32-битного целого числа существует 105'097'565 положительных простых чисел. Вы можете написать программу для их создания или найти в Интернете архивные файлы с ними и использовать их. Затем вы помещаете их в свою программу (например, в файл, который читает ваша программа).

Вы можете сохранить их как упорядоченный массив целых чисел, а затем найти позицию первого простого числа в вашем диапазоне в O(log n) с помощью двоичного поиска (на самом деле это O (1), потому что n — константа 105'097'565). Затем вы просто идете и печатаете массив, пока не получите число, превышающее ваш max.


Соображения сложности

Если вы только начали использовать C, на данном этапе эти параметры могут оказаться для вас слишком сложными.

Однако знать о них полезно, даже если вы не понимаете и половины того, что читаете. Затем, после того, как вы узнаете и попрактикуетесь больше, вы сможете вернуться к ним и попытаться реализовать их. И гордитесь тем, как далеко вы продвинулись 😁

[немного опоздал на главную вечеринку]

Как мне сделать это лучше?

Исправить прайм-чек

check(0) сообщает правду. check(any_negative) сообщает правду. Оба должны сообщить ложь.

Слишком медленно

check(n) повторяется до n раз. Простое изменение будет повторяться не более sqrt(n) раз. Для крупных n это в 10 000 раз быстрее.

Выполняйте итерацию, пока делитель меньше или равен частному.

int prime_check(int number) {
  if (number % 2 == 0) {
    return number == 2;
  }
  // Do not use divisor*divisor <= number as that overflows for a large prime.
  // Good compilers emit efficient code for number / divisor and
  // nearby number % divisor, doing both for the time cost of one.
  for (int divisor = 3; divisor <= number / divisor; divisor += 2) {
    if (number % divisor == 0) {
      return 0;
    }
  }
  return number > 1;
}

Сомнительный макс

Я ожидаю, что max будет частью тестируемого диапазона.

  printf("Enter end of range: ");
  scanf("%d",&max);
  // for (i = min; i < max; i++)
  for (i = min; i <= max; i++)

Но это проблема, когда max == INT_MAX.

Я поделюсь с вами еще одним подходом к достижению вашей цели.

Первое примечание: чтобы избежать проверки «номер 1» при каждом вызове функции проверки, переместите элемент управления if за пределы этой функции и за пределы себя для цикла. Таким образом, вы сможете выполнить эту проверку один раз.

Второе примечание: используйте рекурсивную функцию для создания итеративной проверки каждого числа, которое вы хотите проанализировать.

Функция проверки станет:

int check(int counter, int number)
{
    
    if (number%counter){
        //mod result is not zero
        //increase counter
        counter++;

        //if counter is equal to number
        //it means that number is prime - return 1 to caller!
        //otherwise call check function again with new counter value
        if (counter==number)
            return 1;
        else
            if (check(counter, number))
                //check returns 1 - number is prime! return 1 to caller
                return 1;  
            else
                //check returns 0 - number is not prime! return 0 to caller
                return 0;
    }else{
        //number is not prime
        //return 0 to caller
        return 0;
    }

}

используя рекурсивный вызов, вы можете выполнять итеративные проверки без использования цикла, с точки зрения квантового времени вы можете сэкономить ресурсы и повысить эффективность.

Вот мой полный код с комментариями:

#include <stdio.h>

//declare functions
int check(int counter, int number);

int main()
{
    int i,min,max;
    printf("Enter start of range: ");
    scanf("%d",&min);
    printf("Enter end of range: ");
    scanf("%d",&max);
    
    // if first number is 1, i'll skip it
    if (min == 1){
        min++;
    }

    if (min == 2){
        //2 is prime, print it and increment min
        printf("%d\n",min++);
       
    }

    for (i = min;i <= max;i++)
    {
   
        
        //call recursive function for checking prime
        if (check(2,i))
            printf("%d\n",i);
        

    }
    return 0;
}


/*check function is recursive.
*  it acceptes counter and number values
*  perform prime check:
*    - it's prime number - recursive function stack will be close and return 1
*    - it's not prime number - counter will be increased and check function will be called again from itself
*/  
int check(int counter, int number)
{

    if (number%counter){
        //mod result is not zero
        //increase counter
        counter++;

        //if counter is equal to number
        //it means that number is prime - return 1 to caller!
        //otherwise call check function again with new counter value
        if (counter==number)
            return 1;
        else
            if (check(counter, number))
                //check returns 1 - number is prime! return 1 to caller
                return 1;  
            else
                //check returns 0 - number is not prime! return 0 to caller
                return 0;
    }else{
        //number is not prime
        //return 0 to caller
        return 0;
    }

}

Плюсы: очищенная компоновка кода, меньше затрат времени.

Минусы: немного сложнее, меньше используется стековая память.

Последние соображения: для дополнительных улучшений вам следует реализовать элементы управления входными данными (минимальные и максимальные значения), чтобы избежать непредвиденного поведения.

Обновлено: я изменил указатели удаления кода и перемещение использования памяти стека в минусах.

Боюсь, этот подход крайне неэффективен и некорректно обрабатывает числа ниже 2: ни одно из этих чисел не является простым. Ваш код печатает 1 как простое число и долго выполняет цикл для 0 и отрицательных чисел, прежде чем вызвать неопределенное поведение.

chqrlie 30.08.2024 17:40

используя рекурсивный вызов, вы можете выполнять итеративные проверки без использования цикла. С точки зрения квантового времени вы можете сэкономить ресурсы и повысить эффективность, где же этому учат? Более того, нет абсолютно никакой необходимости использовать указатели, и counter++ не увеличивает счетчик, а просто увеличивает указатель, вызывая неопределенное поведение для любого составного числа. Кроме того, меньшее использование памяти стека является неправильным, большее использование стека более вероятно, и это мошенничество.

chqrlie 30.08.2024 17:43

«Я использую указатель вместо переменной, чтобы использовать меньше памяти в стеке при каждом вызове функции проверки» — это совершенно неверно.

Sneftel 30.08.2024 18:01

Я прошу прощения за мой неправильный код. Он содержит много ошибок (да, число 1 не простое, но 2 простое, и мой код его не распознает). Более того, в этом случае я мог бы реализовать рекурсивную функцию без указателей. Я понял.

N3tMaster 30.08.2024 18:51

N3tMaster, если бы number было большим простым числом, скажем, около 1 000 000 000, я бы ожидал, что check() повторится примерно 1 000 000 000 раз, прежде чем развернуться и сообщить о простом числе. Такая глубина стека приведет к переполнению стека на многих машинах. ИМХО, рекурсия, используемая здесь, не является правильным подходом.

chux - Reinstate Monica 30.08.2024 18:55

@chux-ReinstateMonica, да, ты прав. я не лучшее решение при работе с большим количеством чисел

N3tMaster 30.08.2024 19:06

Несколько лет назад я взял это из книги, возможно, «Алгоритмы на C», в которой sqrt() и floor() используются для определения момента разрыва цикла, с несколькими моими небольшими дополнениями:

/* return 1 if n is a prime number  */
int is_prime(unsigned int n) {
    unsigned int spia,f;
    if ( !n ) return 0;
    if ( n <= 3 ) return 1;
    if ( (n % 2) && (n % 3) ) {
      spia = (unsigned int) floor(sqrt(n));
      for ( f=5 ; (n % f) && (n % (f+2)) && f <= spia ; f+=6 ) ;
      if ( f > spia ) return 1;
    } // endif
    return 0;
}

Основываясь на предыдущих ответах, я проверил эту версию с удаленным sqrt() и замененным на / (целочисленное деление), чтобы посмотреть, когда разорвать цикл:

int is_prime_w_div(unsigned int n) {
    unsigned int f;
    if ( !n ) return 0;
    if ( n <= 3 ) return 1;
    if ( (n % 2) && (n % 3) ) {
      for ( f=5 ; (n % f) && (n % (f+2)) ; f+=6 ) {
            if ( f > n/f ) break;
      } // endfor
      if ( f > n/f ) return 1;
    }
    return 0;
}

Я замерил обе версии по 10 миллионам целых чисел (от 0 до 10000000) как на Raspi400 под управлением Raspbian Linux, так и на Geekom Mini IT13 Core i9-13900H под управлением OpenSuse Leap15.6, и в обоих случаях версия sqrt() была самой быстрой:

/* testprime.c */
#include <stdio.h>
#include <math.h>

/* put is_prime and is_prime_w_div here */

int main(void) {
  unsigned int i,cnt;
  for ( cnt=0,i=0 ; i<10000000 ; i++) {
    if (is_prime(i)) cnt++;
    /* if (is_prime_w_div(i)) cnt++; */
  } /* endif */
  printf("found %u primes\n",cnt);
  return 0;
}

/*
 * gcc testprime.c -lm
 * time ./a.out
 */ 

Так что за история? На современном оборудовании математические операции с плавающей запятой выполняются аппаратно, и sqrt() НЕ вызывается в цикле на каждой итерации. Или я что-то еще упускаю?

Другие вопросы по теме

Нахождение максимального произведения элемента массива и расстояния
Эффективное интервальное хранение с использованием стандартной библиотеки C++
Самая длинная полная подпоследовательность упорядоченных гласных
Есть ли способ получить проекцию на заархивированные векторы std::tuple без лямбды?
Как оптимально разместить две точки на числовой прямой, чтобы минимизировать общее расстояние до набора заданных точек
Есть ли в этом алгоритме преобразования двоичного дерева поиска в отсортированный связанный список в Википедии ошибка?
Почему на этапе разделения быстрой сортировки используется одно условие остановки с двумя указателями L и R, а (L <= R)? Может ли это быть пока (L < R)?
Учитывая двоичную строку, подсчитайте количество плотных подстрок за время O (nlogn)
Диаметр двоичного дерева — неудачный случай, когда самый длинный путь не проходит через корень
Реализация Java, проблема производительности алгоритма Динича