Я начал изучать 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, как и я.
За исключением лучшего (то есть «менее общего») имени функции (возможно, is_prime
или подобного), с этой функцией все в порядке. Ваши недостатки находятся в main
, где вы всегда должны проверять возвращаемое значение scanf
Вы можете улучшить основной цикл, начав с нечетного числа, ближайшего к min
, и каждый раз увеличивая счетчик циклов на два. Проверять четные числа нет смысла, так как они (кроме 2) не могут быть простыми. Кроме того, в вашем методе check
вам нужно посчитать только до (number /2)
, так как к этому времени все действительные деления будут завершены.
Вам нужно проверить только sqrt (число)
@pm100 введение чисел с плавающей запятой в по существу целочисленную задачу создает больше проблем, чем необходимо.
Как сделать это лучше? Всегда проверяйте возврат каждой функции ввода, чтобы убедиться, что ввод выполнен успешно или неудачно.
Вероятно, самое большое улучшение производительности можно было бы получить, если бы тест на деление на 2 был создан с использованием if (!(number & 1)) return 0
, а затем каждый раз начинался отсчет с 3
и увеличивался на count += 2
. Также измените условие цикла на while (count*count < number)
. И то, и другое предотвращает много ненужной работы. Немного более сложная схема с чередованием приращений 2
и 4
позволяет пропустить все числа, делящиеся на три. Общая эвристика заключается в том, что деление — это самая затратная операция: почти каждая вторая двоичная операция занимает один машинный цикл на современном процессоре.
@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
позволяет избежать этого и требует небольших затрат для компилятора, который выполняет /
и %
вместе.
Вы должны опубликовать это на codereview.stackexchange.com.
@chux Последняя машина, которую я использовал, которая не была дополнением к двум, была еще в 1984 году. Есть ли какие-либо современные процессоры, которые все еще имеют знаковую величину (возможно, чипы DSP?) или дополняют их в наши дни? Я допускаю, что существует возможность переполнения, но это может произойти только в том случае, если число является простым числом Мерсенна в форме 2^N-1
, где N — размер машинного целого числа в битах. Так что я думаю, что для 32-битных целых чисел это не удастся, поскольку 2^31-1
является простым числом. В противном случае числа этой формы являются составными и обычно делятся хотя бы на одно из 3,5,7. Еще одно тривиальное улучшение программы — использование unsigned int.
@MartinBrown 1) Проблема с дополнением, отличным от 2, отложена в сторону. number % 2 == 0
остается более ясным, чем !(number & 1)
2) «возможность переполнения, но это может произойти только в том случае, если число является простым числом Мерсенна» -> divisor * divisor
переполняется, когда number == 2147483629
или 2147483549
или другие выбранные большие простые числа, даже если не простое число Мерсенна.
@chux, ты прав. Я не заметил этой подвоха.
Есть Code Review@SE.
Извиняюсь @greybeard, я не знал, что такое существует.
Ничего, если я явно преобразую квадратный корень в целое число и буду выполнять итерацию до этого целого числа?
Это одна из проблем, с которой сталкиваются все начинающие программисты: как проверить, является ли число простым, и обычно идея заключается в следующем:
Также проверяйте только по модулю нечетные числа, кроме 2.
@Fredrik: не стесняйтесь изменять мой ответ, он может стать ссылкой на вопросы такого типа :-)
На самом деле вам не нужно вычислять квадратный корень... просто увеличивайте число до тех пор, пока проверяемое число, умноженное само на себя, не превысит предел.
Я думаю, что это на самом деле не отвечает на заданный вопрос, в котором основное внимание уделяется «хочу знать, есть ли лучший способ вернуть 0, если введенное число равно 1 или нет, как мне кажется, это код изоленты». Я думаю, что код ОП соответствует принятому определению, а ваши шаги — нет. Кроме того, меня не устраивает точность формулировки последних шагов, которую можно прочитать так, что соответствующий код имеет ошибку на единицу.
@pmg "идите до тех пор, пока число, которое вы проверяете, умноженное само на себя, не превысит предел". имеет недостаток. Он переполняется, когда limit является большим простым числом.
Верно, @chux, спасибо. ОП, я исправляю свой комментарий на «подниматься до n > limit / n
»
Является ли проверка на частное быстрее, чем итерация до извлечения квадратного корня?
Более полезно, если вы хотите стать программистом на языке C... Что произойдет, если я введу «лягушку» в качестве минимального значения. Теперь вы входите в мир обнаружения и восстановления ошибок.
Немного запутанно, но я воспринимаю это как ответ на вопрос «Как улучшить этот код?». ;-)
«разговорчивый», возможно, означает «проверяйте каждый вводимый возврат или рискуйте UB» :)
«Что произойдет, если я введу «лягушку» в качестве минимального значения?» Счетчик программы безоговорочно перейдет в ближайший пул памяти.
Я об этом не подумал, спасибо.
Проверка возврата от функций ввода важна. Также возможно решить эту проблему путем инициализации таких переменных, как min
и max
, при их объявлении, вместо того, чтобы рассчитывать на то, что scanf
сделает это.
Есть несколько способов улучшить сам алгоритм обнаружения простых чисел. Ранее на него уже давался подробный ответ, например. здесь, поэтому не буду углубляться в это.
Далее возникает вопрос о том, как проверить ввод. 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
.0
в случае ложности и ненулевое значение в случае истины. Просто тестирование if (isprime(i))
более читабельно.if (n < 2)
вместо if (n == 1)
. Если вы не можете предположить, что аргумент n
равен как минимум 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)»?
Является ли использование оператора if единственным способом проверить исключаемые числа ниже 2?
@nkminion «Использование оператора if — единственный способ проверить...» --> Нет. Альтернатива: вернуть сравнение.
Это мелочь, но, вероятно, эти "invalid input\n"
сообщения будут распечатаны на stderr
, а не на stdout
.
Помимо проверки того, является ли каждое число в диапазоне простым, есть и другие подходы:
Это алгоритм генерации всех простых чисел от 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
и отрицательных чисел, прежде чем вызвать неопределенное поведение.
используя рекурсивный вызов, вы можете выполнять итеративные проверки без использования цикла. С точки зрения квантового времени вы можете сэкономить ресурсы и повысить эффективность, где же этому учат? Более того, нет абсолютно никакой необходимости использовать указатели, и counter++
не увеличивает счетчик, а просто увеличивает указатель, вызывая неопределенное поведение для любого составного числа. Кроме того, меньшее использование памяти стека является неправильным, большее использование стека более вероятно, и это мошенничество.
«Я использую указатель вместо переменной, чтобы использовать меньше памяти в стеке при каждом вызове функции проверки» — это совершенно неверно.
Я прошу прощения за мой неправильный код. Он содержит много ошибок (да, число 1 не простое, но 2 простое, и мой код его не распознает). Более того, в этом случае я мог бы реализовать рекурсивную функцию без указателей. Я понял.
N3tMaster, если бы number
было большим простым числом, скажем, около 1 000 000 000, я бы ожидал, что check()
повторится примерно 1 000 000 000 раз, прежде чем развернуться и сообщить о простом числе. Такая глубина стека приведет к переполнению стека на многих машинах. ИМХО, рекурсия, используемая здесь, не является правильным подходом.
@chux-ReinstateMonica, да, ты прав. я не лучшее решение при работе с большим количеством чисел
Несколько лет назад я взял это из книги, возможно, «Алгоритмы на 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() НЕ вызывается в цикле на каждой итерации. Или я что-то еще упускаю?
То, что вы написали, совершенно нормально. Его легко читать и понимать, а компилятору также легко его оптимизировать при необходимости. Единственное изменение, которое я бы сделал, — это заменить цикл в функции
check
на циклfor
.