qsort function - trying to use a comparator

后端 未结 3 1986
执念已碎
执念已碎 2021-01-23 10:54

I made a qsort function for a larger program. It is to sort by time. I have a class schedule I am working with and found a need to compare time with AM and PM. ie if A is chosen

相关标签:
3条回答
  • 2021-01-23 11:21

    Writing a comparator

    In C, the comparator function from qsort() returns a number less than, equal to, or greater than zero according to whether the data structure represented by the first argument should be sorted before, equal to, or after the second parameter.

    Sorting am and pm times is painful; even converting from am/pm to 24-hour is not entirely trivial. It is far better to store time values in 24 hour notations (or even as seconds since The Epoch). The presentation layer should deal with presenting the time in am/pm notation; the model and controller layers should usually avoid messing with am/pm. Don't forget:

    12:01 am happens 1 hour   before  1:01 am
    11:59 am happens 1 minute before 12:00 pm
    12:00 pm happens 1 hour   before  1:00 pm
    

    Assuming you are not constrained to events starting on the hour and that you have resolved to use 24 hour times internally, then you can write code such as:

    int SchedRecordTimeComparator(void const *v1, void const *v2)
    {
        sched_record const *r1 = v1;  /* I don't mind a cast; there are those who do */
        sched_record const *r2 = v2;
        if (r1->start.hour < r2->start.hour)
            return -1;
        else if (r1->start.hour > r2->start.hour)
            return +1;
        else if (r1->start.minute < r2->start.minute)
            return -1;
        else if (r1->start.minute > r2->start.minute)
            return +1;
        else
            return  0;
    }
    

    It is fairly obvious how to extend that to manage seconds or other match criteria. Note that this code does not run the risk of integer overflow, whereas subtracting two numbers could in general run into overflow problems.

    If you decide to continue with 12 hour clocks, then you will have to have a way to distinguish 6 am from 6 pm. Basically, you'll convert your 12 hour notation into 24 hour notation in the function, then do the comparison on that basis (I'm assuming AM and PM are enumeration constants):

    int SchedRecordTimeComparator(void const *v1, void const *v2)
    {
        sched_record const *r1 = v1;  /* I don't mind a cast; there are those who do */
        sched_record const *r2 = v2;
        int hhmm1 = ((r1->start.hour % 12) + (r1->start.am_pm == AM ? 0 : 12)) * 60 +
                      r1->start.minute;
        int hhmm2 = ((r2->start.hour % 12) + (r2->start.am_pm == PM ? 0 : 12)) * 60 +
                      r2->start.minute;
        if (hhmm1 < hhmm2)
            return -1;
        else if (hhmm1 > hhmm2)
            return +1;
        else
            return  0;
    }
    

    How might this be used?

    I am not really sure on how to use it?

    Usage is fairly simple. Somewhere in your program, you have an array of sched_records:

    sched_record *array = malloc(num_records * sizeof(*array));
    ...
    ...fill up array with data...
    ...
    qsort(array, num_records, sizeof(*array), SchedRecordTimeComparator);
    ...
    

    That's all there is to it. It could be a fixed size array instead:

    sched_record array[NUM_RECORDS];
    

    Then, assuming you still have a variable size_t num_records which indicates how many of the records are in use, you use the same qsort() call. Using qsort() is very straight-forward. Using bsearch() is slightly more complex, because you typically have to fake up a record to find:

    sched_record *SchedRecordSearch(int hour, int minute, sched_record *array, size_t num_records)
    {
        sched_record key = { .start.hour = hour, .start.minute = minute };
        return bsearch(&key, array, num_records, sizeof(*array), SchedRecordTimeComparator);
    }
    

    Using C99 and designated initializers makes it easier. You have to ensure that your key record has an appropriate value in each of the fields that will be used by the comparator. Of course, you've already sorted the array with qsort() before you use bsearch() on it — or made sure that the data is in the same sorted order 'as if' you had done a qsort() on it with the same comparator.

    It's also worth writing a function to check the sort order of an array — it is straight-forward, and left as an 'exercise for the reader'. You can then use that in assertions, for example.


    Don't write your own qsort()

    I note that all of us answering the question are assuming that you are using the Standard C Library sort function, yet your question suggests you've written your own. Generally speaking, you'll have to be very good to do better than the system-provided qsort(); I wouldn't bother to write my own unless I could demonstrate that the system function was too slow.

    • Use the system-provided qsort() until you don't need to ask how to write one for yourself.

    If you must still write the code, then you need to decide on the interface. You can either mimic the standard interface (but use a different name), or you can write a custom interface tied to one specific type (which must be re-parameterized if you need to sort a different type). The latter is roughly what C++ does with templates.

    One of the problems for writing your own generic comparator is swapping the elements when you don't know how big the elements will be in advance. If you can use C99 and VLAs, it isn't too bad, though if the size of an element blows your stack, then you're completely hosed.

    Inside the function, you have to be careful to use char * instead of void * because you can't legitimately do pointer arithmetic on void *, notwithstanding that GCC does allow you to do so as a non-standard extension.

    You need to make sure you have a clear picture of how the data is laid out, and what your sorting code will be doing to it. You'll take a comparator like the ones described in the various answers, and when you need to do a comparison, you'll do something like:

     int cmp = (*Comparator)(char_base + (i * element_size), char_base + (j * element_size));
    

    You can then do:

     if (cmp < 0)
         act on "element i smaller than element j"
     else if (cmp > 0)
         act on "element i greater than element j"
     else
         act on "elements i and j are equal"
    

    Showing Different Ranges

    I'm not sure it would do what I want. I have to look more closely. My sort function I originally posted did sort by time from earliest to latest. I may not have been clear on my question. In the menu portion of my program, I have an option line of sorting by AM, PM or Don't care. A is for classes starting before 12:00 PM, P is for classes starting at noon or later, and D for don't care. If the user chooses A, than list the classes up to 12:00, etc. If yours does that, than how do I make that distinction?

    You're confusing two things: sorting the data and presenting the right subset of the data. You sort the data as discussed/shown. This gives you a sorted array. Then you scan through the array to present the entries in the time range you are interested in. That will be a separate function; you might well still use the comparator function, but you'd create a pair of dummy keys for the start and end of the time range (each key would be a bit like the key in the bsearch() example in my answer) and then look for all the records in the sorted array after the start time and before the end time.

    I'm about to make some simplifying assumptions. Your start.hour records the time unambiguously as a number of minutes since midnight, and it is an integer.

    1. Sort the array:

      qsort(array, num_records, sizeof(*array), SchedRecordTimeComparator);
      
    2. Generate the correct keys — lo and hi:

      typedef struct sched_range
      {
          sched_record *lo;
          sched_record *hi;
      } sched_range;
      
      sched_record lo, hi;
      if (choice == 'A')
      {
          lo.start.hour = 0;        /* Midnight (am) */
          hi.start.hour = 12 * 60;  /* Midday */
      }
      else if (choice == 'D')
      {
          lo.start.hour = 12 * 60;  /* Midday */
          hi.start.hour = 24 * 60;  /* Midnight (pm) */
      }
      else
      {
          lo.start.hour = 0;        /* Midnight (am) */
          hi.start.hour = 24 * 60;  /* Midnight (pm) */
      }
      
    3. Write the SchedRangeSearch() function:

      sched_range SchedRangeSearch(sched_record const *array, size_t num_records,
                      sched_record *lo, sched_record *hi,
                      int (*comparator)(void const *v1, void const *v2))
      {
           sched_range r = { 0, 0 };
           sched_record const *ptr = array;
           sched_record const *end = array + num_records;
      
           /* Skip records before start time */
           while (ptr < end && (*comparator)(lo, ptr) < 0)
               ptr++;
           if (ptr >= end)
               return r;  /* No matching records */
      
           r.lo = ptr;  /* First record in range */
      
           /* Find first record after finish time - if any */
           while (ptr < end && (*comparator)(ptr, hi) < 0)
               ptr++;
      
           r.hi = ptr;
           return r;
      }
      
    4. Use the search function to find the range required:

      sched_range r = SchedRangeSearch(array, num_records, &lo, &hi, SchedRecordTimeComparator);
      
    5. Show the relevant records:

      if (r.lo != 0)
      {
          assert(r.hi != 0);
          sched_record *ptr;
      
          for (ptr = r.lo; ptr < r.hi; ptr++)
              show_sched_record(ptr);
      }
      else
          show_empty_schedule();
      

    Untested code: beware crashes, out of bounds access, etc.

    The intention is that the search function provides two pointers, to the start (first valid item in the range) and end of the range, where the end pointer points beyond the last valid item. Hence the for loop to display the valid data goes from start to strictly less than end. (This is the same convention used in C++ with STL iterators. It pays to reuse good ideas.)

    0 讨论(0)
  • 2021-01-23 11:25

    Just add separate check for AM vs PM and make any AM time be less than PM time:

    int sortFunction(const void *p, const void *q) {
      return
        (sched_record *) p)->am_pm < (sched_record *) q)->am_pm ?
          -1 :
        (sched_record *) p)->am_pm > (sched_record *) q)->am_pm ?
           1 :
           ((sched_record *) p)->start.hour -
                   ((sched_record *) q)->start.hour;
    }
    

    presumably in your sched_record the am_pm field will hold 1 for am and 2 for pm, or something like that.

    edit: turns out the OP doesn't have am_pm indicator in their structure and so must probably use 24 hour time representation, apparently with integers for hours and minutes:

    int sortFunction(const void *p, const void *q) {
      return
        (sched_record *) p)->start.hour < (sched_record *) q)->start.hour ?
          -1 :
        (sched_record *) p)->start.hour > (sched_record *) q)->start.hour ?
           1 :
           ((sched_record *) p)->start.minutes -
                   ((sched_record *) q)->start.minutes;
    }
    
    0 讨论(0)
  • 2021-01-23 11:31

    Assuming you have a function greaterThan, you can implement sortFunction as follows:

    int sortFunction(const void *p, const void *q) {
        if (greaterThan(p, q)) { // p > q
            return +1;
        } else if (greaterThan(q, p)) { // p < q
            return -1;
        } else { // p == q
            return  0;
        }
    }
    
    0 讨论(0)
提交回复
热议问题