Как отсортировать массив `int **` в C с помощью собственного qsort

Мне не удалось найти ни одного вопроса по этому поводу, и я думаю, что немного схожу с ума, пытаясь понять это.

У меня есть следующий код:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>

int cmp_int(const void *a, const void *b)
{
  return * (int *)a - * (int *)b;
}

int main(int argc, char *argv[])
{
  int n = 10;
  int **arr = calloc(n, sizeof(int *));
  srand((unsigned int) time(NULL));
  for (int i = n-1; i >= 0; i--) {
    arr[i] = calloc(1, sizeof(int));
    *(arr[i]) = rand() % 1000;
  }
  for (int i = 0; i < n; i++)
    printf("%d ", *(arr[i]));
  printf("\n");
  qsort(arr, 10, sizeof(void *), cmp_int);
  for (int i = 0; i < n; i++)
    printf("%d ", *(arr[i]));
  printf("\n");
  free(arr);
  return 0;
}

Это очень просто, верно? Согласно справочной странице, первый аргумент — это указатель на базовый элемент, а третий аргумент — размер. Однако мне не удается получить массив в виде отсортированного результата. Я все еще не совсем понимаю, какими должны быть первый и третий аргументы для qsort, так как я подозреваю, что именно в этом и заключается ошибка.

Любая помощь приветствуется.

Спасибо.

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


person S. Sharma    schedule 24.01.2020    source источник
comment
Ваша функция сравнения неверна. Вы сортируете массив указателей на int. Функция сравнения получит указатели на эти указатели. Таким образом, сравнение должно быть return **(int **)a - **(int **)b; Несмотря на то, что это должно заставить программу работать, в ней есть много странностей и проблем.   -  person Gene    schedule 24.01.2020
comment
@Gene таким образом, вы все равно будете страдать от переполнения и UB. Например, если a = 2147483640, b = -2147483642, то a - b = -14 (если результат повторяется), что определенно неверно. То же самое для a = -2147483642 и b = 2147483640, в которых a - b будет положительным. Вам нужно выполнить вычитание без знака, чтобы избежать переполнения, и использовать идиоматическую функцию сравнения return (x < y) - (x > y). Таким образом, оператор станет return (**(unsigned **)a < **(unsigned **)b) - (**(unsigned **)a > **(unsigned **)b))   -  person phuclv    schedule 24.01.2020
comment
@phuclv, почему unsigned, когда значения имеют тип int?   -  person chmike    schedule 24.01.2020
comment
@chmike да, моя ошибка. Я имел в виду без знака, когда делал вычитание, например return **(int **)a - **(int **)b;. Но так как это не сработает, вам все равно придется сделать 2 сравнения, как указано выше, в int, а не unsigned int.   -  person phuclv    schedule 24.01.2020


Ответы (2)


Проблема, с которой вы столкнулись, заключается в том, что вы не учитываете один дополнительный уровень косвенности, созданный путем выделения блока указателей с помощью int **arr = calloc (n, sizeof *arr);, а затем выделения памяти для одного int каждому указателю с arr[i] = calloc (1, sizeof *arr[i]).

Поскольку функция сравнения int compare (const void *a, const void *b) для qsort ожидает указатель на элементы сортируемого массива, и a, и b выше будут указатель-указатель до int в вашем случае. требуется разыменование 2 уровней косвенности, прежде чем можно будет сравнивать целочисленные значения.

Вместо cmp_int вам действительно нужна функция сравнения cmp_int_ptr. Это можно записать как:

int cmp_int_ptr (const void *a, const void *b)
{
    int *ai = *(int * const *)a,
        *bi = *(int * const *)b;

    return (*ai > *bi) - (*ai < *bi);
}

(примечание: два уровня косвенности в приведении (int * const *)..., которые также могут быть записаны как (int **), но для соответствия типу параметра (const void *) правильным будет (int * const *))

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

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>

int cmp_int_ptr (const void *a, const void *b)
{
    int *ai = *(int * const *)a,
        *bi = *(int * const *)b;

    return (*ai > *bi) - (*ai < *bi);
}

int main (void) {

    int n = 10;
    int **arr = calloc (n, sizeof *arr);

    if (!arr) {
        perror ("calloc-arr");
        return 1;
    }

    srand((unsigned int) time(NULL));

    for (int i = 0; i < n; i++) {
        if (!(arr[i] = calloc (1, sizeof *arr[i]))) {
            perror ("calloc-arr[i]");
            return 1;
        }
        *(arr[i]) = rand() % 1000;
    }

    for (int i = 0; i < n; i++)
        printf (" %d", *(arr[i]));
    putchar ('\n');

    qsort (arr, 10, sizeof *arr, cmp_int_ptr);

    for (int i = 0; i < n; i++) {
        printf (" %d", *(arr[i]));
        free (arr[i]);              /* don't forget to free your int allocated */
    }
    putchar ('\n');

    free(arr);                      /* now free pointers */
}

Пример использования/вывода

$ ./bin/qsortptrtoint
 654 99 402 264 680 534 155 533 397 678
 99 155 264 397 402 533 534 654 678 680

Просмотрите вещи и дайте мне знать, если у вас есть вопросы.

person David C. Rankin    schedule 24.01.2020

От вашей программы у меня болит голова. Причина, по которой вы не получаете правильную сортировку, заключается в том, что функция сравнения неверна. Это должно быть return **(int **)a - **(int **)b;, чтобы получить правильный результат.

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

  • Если вы не используете argc и argv, не объявляйте их.
  • Вводить вызов srand не нужно.
  • int сравнение методом вычитания — плохая идея, потому что оно может переполниться.
  • calloc всегда следует проверять на нулевые (недостаточно памяти) результаты.
  • calloc вообще не нужен. Используйте массив переменной длины.
  • Нет необходимости выделять массив указателей на целые числа. Просто выделите массив целых чисел. Тогда ваше сравнение работает как есть.
  • Вызов qsort использует жесткую константу 10, а не n.
  • Менее подвержено ошибкам указание размера элемента путем разыменования имени массива.
  • В конце вы освобождаете массив "spine", но никогда не целочисленные элементы.
  • Вы должны выделить функцию для печати массива.

Вот версия, которая решает эти проблемы.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int cmp_int(const void *va, const void *vb)
{
  int a = *(int *)va, b = *(int *) vb;
  return a < b ? -1 : a > b ? +1 : 0;
}

void print(int *a, int n) {
  for (int i = 0; i < n; ++i) printf("%d ", a[i]);
  printf("\n");
}

int main(void)
{
  int n = 10, a[n];
  srand(time(0));
  for (int i = 0; i < n; ++i) a[i] = rand() % 1000;
  print(a, n);
  qsort(a, n, sizeof a[0], cmp_int);
  print(a, n);
  return 0;
}
person Gene    schedule 24.01.2020
comment
Может ли первая проблема (экспресс argc, argv) вызвать проблему или это просто предложение? - person EsmaeelE; 24.01.2020
comment
@EsmaeelE: если ваш компилятор не жалуется на неиспользуемые переменные, когда вы используете int main(int argc, char **argv), а затем не используете argc или argv, значит, у вас недостаточно предупреждений. Вам нужна любая помощь, которую вы можете убедить компилятор предоставить вам. - person Jonathan Leffler; 24.01.2020
comment
Привет, спасибо за предложения. Я добавил пояснение к своему сообщению выше (также я думаю, что, возможно, непреднамеренно отредактировал ваше сообщение на своем телефоне). - person S. Sharma; 24.01.2020
comment
@JonathanLeffler Хорошо, я нахожу это. gcc с -Wextra поймать неиспользуемые параметры. - person EsmaeelE; 24.01.2020
comment
@JonathanLeffler Есть ли какая-либо ссылка или пример, показывающий, что если мы выражаем параметры функции и не используем их (проходим мимо, а не используем в теле функции), особенно для основной, то есть main(int argc, char **argv) вызывает проблему? Когда main определяете таким образом main(int argc, char **argv) и не передаете никаких аргументов из консоли. Передают ли ему какой-либо аргумент по умолчанию? - person EsmaeelE; 27.01.2020
comment
На одном уровне это не вызывает никаких проблем. На другом уровне стоимость помещения параметров в стек невелика, и эти затраты тратятся впустую, если параметры не используются. С main() вы обычно не вызываете его явно, поэтому штраф незначителен, тем более что библиотека времени выполнения все равно подталкивает значения. Но в тесном цикле вам могут не понадобиться накладные расходы. Но передача неиспользуемых параметров попахивает небрежностью. Если вы ограничены внешним дизайном (он должен соответствовать прототипу интерфейса), вы плывете по течению. C++ позволяет оставлять неиспользуемые параметры безымянными, что очень удобно. - person Jonathan Leffler; 27.01.2020