C: Merge-Sort of an array with uneven number of elements

不羁的心 提交于 2019-12-12 22:30:49

问题


I've been working on an assignment for my Procedural Programming class where we are provided with a merge-sort program that does not function completely. It performs a merge-sort on arrays with an even number of integers, but throws a segmentation fault with an odd number of integers.

I understand how the sorting works, and that the segmentation fault is being thrown because the odd number is causing the segmentation fault because the array is being over-filled somehow. I also understand that the solution is going to involve a test for whether or not the original array is even or odd, and then pass the values to the merge function differently depending on this. Despite what I do understand about the program, I have been banging my head against the wall for weeks trying to get this to work properly, and I'm hoping someone can give me some advice.

I've done a lot of looking around for answers before posting this, but all other examples involve merge-sort programs with structs, which is beyond what I've learned so far. You'll see in the code I post below. Also, the full program involves a few other files, but I've included just the mergesort.c file and the merge.c file which, as I've been assured by my professor, are the only places any changes need to be made. The main file works perfectly and is only responsible for filling the array and calling the mergesort function. If the other files are necessary, let me know and I'll post them. The only reason I haven't is because we are using a Linux shell, and I haven't found a practical way to copy and paste code from the shell to my own operating system, and it takes a while to write it out.

Thanks in advance for any pointers you can provide. Here is the code.

mergesort.c

#include <"mergesort.h">

void mergesort(int key[], int n) //key is the array, n is the size of key
{
    int j, k, m, *w;

    w = calloc(n, sizeof(int));
    assert(w != NULL);

    for (k = 1; k < n; k *= 2) {
        for (j = 0; j < n - k; j += 2 * k) {
            merge(key + j, key + j + k, w + j, k, k);
        }
        for (j = 0; j < n; ++j) {
            key[j] = w[j];
        }   
    }
    free(w);
}

merge.c

#include "mergesort.h"

void merge(int a[], int b[], int c[], int m, int n) {
    int i = 0, j = 0, k = 0;

    while (i < m && j < n) {
        if (a[i] < b[j]) {
            c[k++] = a[i++];
        } else {
            c[k++] = b[j++];
        }   
    }

    while (i < m) {
        c[k++] = a[i++];
    }
    while (j < n) {
        c[k++] = b[j++];
    }   
}

回答1:


Your code has some problems:

  • The include preprocessor directive is incorrect, either use #include "mergesort.h" or #include <mergesort.h>.

  • You must compute the size of the arrays passed to merge() correctly so it does not read beyond the end of the last chunk. As currently coded, n must be a power of 2 to avoid undefined behavior.

Here is a corrected version of mergesort.c for your purpose:

#include "mergesort.h"

void mergesort(int key[], int n) {
    // key is the array, n is the number of elements
    int i, j, k, m;
    int *w;

    // allocate the working array
    w = calloc(n, sizeof(int));
    // abort the program on allocation failure
    assert(w != NULL);

    // for pairs of chunks of increasing sizes
    for (k = 1; k < n; k *= 2) {
        // as long as there are enough elements for a pair
        for (j = 0; j + k < n; j = j + k + m) {
            // compute the size of the second chunk: default to k
            m = k;
            if (j + k + m > n) {
                // chunk is the last one, size may be smaller than k
                m = n - j - k;
            }
            // merge adjacent chunks into the working array
            merge(key + j, key + j + k, w + j, k, m);
            // copy the resulting sorted list back to the key array
            for (i = 0; i < k + m; i++) {
                key[j + i] = w[j + i];
            }
        }
    }
    free(w);
}

Here are some additional remarks about this exercise, but you might not be advanced enough and changing the API is probably not allowed:

  • Using 2 different source files seems overkill. The merge routine is an auxiliary function that deserves to be static. It will be expanded inline by modern compilers.

  • Array sizes should be passed as size_t just after the corresponding pointer (for consistency).

  • Instead of asserting the allocation success, you should return a failure code and let the caller handler the failure gracefully.

  • You can use the start of the working array for all merge operations. This improves cache efficiency.

Here is a version with all these changes:

#include "mergesort.h"

static void merge(int a[], size_t m, int b[], size_t n, int c[]) {
    size_t i = 0, j = 0, k = 0;

    while (i < m && j < n) {
        if (a[i] < b[j]) {
            c[k++] = a[i++];
        } else {
            c[k++] = b[j++];
        }
    }
    while (i < m) {
        c[k++] = a[i++];
    }
    while (j < n) {
        c[k++] = b[j++];
    }
}

int mergesort(int key[], size_t n) { 
    // key is the array, n is the size of key
    // return 0 for success, -1 for failure with error code in errno
    size_t i, j, k, m;
    int *w;

    w = calloc(n, sizeof(int));
    if (w == NULL)
        return -1;

    for (k = 1; k < n; k *= 2) {
        for (j = 0; j + k < n; j += k + m) {
            m = k;
            if (j + k + m > n) {
                m = n - j - k;
            }
            merge(key + j, k, key + j + k, m, w + j);
            // copy the sorted chunk back to the key array
            for (i = 0; i < k + m; i++) {
                key[j + i] = w[i];
            }
        }
    }
    free(w);
    return 0;
}

You can further improve the implementation by removing almost half the tests on the index variables in function merge():

static void merge(int a[], size_t m, int b[], size_t n, int c[]) {
    /* always called with m > 0 and n > 0 */
    for (size_t i = 0, j = 0, k = 0;;) {
        if (a[i] < b[j]) {
            c[k++] = a[i++];
            if (i == m) {
                while (j < n) {
                    c[k++] = b[j++];
                }
                break;
            }
        } else {
            c[k++] = b[j++];
            if (j == n) {
                while (i < m) {
                    c[k++] = a[i++];
                }
                break;
            }
        }
    }
}

You can improve mergesort and merge with these further ideas:

  • comparing the last element of a and the first element of b in merge allows vast speed improvements on partially or totally sorted arrays.

  • merge could return the number of elements to copy back, removing all copying in the sorted case.

  • by copying the left chunk to the temporary array and merging into the key array, you can reduce the size of the temporary array.

  • merging balanced chunk sizes instead of powers of 2 reduces the total number of comparisons for non power of 2 array sizes, but it is easier to implement with a recursive approach.




回答2:


So I've found where your the segmentation error is comming from. If you take a closer look to the first inner for-loop in your mergesort:

        for(j = 0; j < n - k; j += 2 * k)
        {
            merge(key + j, key + j + k, w + j, k, k);
        }  

you'll notice that the condition does not really coincide with what you are giving to the merge function as boundaries for your slices of the array. The condition is j < n - k so the max value of j is n - k - 1. But in the arguments of your merge the second array-slice you pass starts at key + j + k and you tell it has size k, so you end up at index j + k + k - 1, if you replace your j by its maximum value you get n - k - 1 + k + k - 1 = n. Which means that you are telling the merge-function he can go till index n. Since the size of key is n, it has no index n. So how do you have to rewrite your condition? We just calculated the maximum index the merge will be accessing: j + k + k - 1. So it means you just have to set j + k + k - 1 < n as condition. this means:

        for(j = 0; j <= n - (k*2); j += 2 * k)
        {
            merge(key + j, key + j + k, w + j, k, k);
        } 

Now we got rid of the segmentation faults, we can go to the second part: making it work for all sizes. The reason why it does only work for sizes wich are a power of 2 (not even all even sizes: try sorting this [2, 3, 5, 6, 4, 1] you'll see) is because of your k. It is k that sets the determines the size of the slices that will be merged in the loop. k gets multiplied by 2 after each round, so it will only gets sizes that are a power of 2! When it's not a power of 2, it will just ignore the part that gets "over" the power of 2...if you get what I mean? Before we made that change that solved the segmentation error it would have just tried to do it but fail for that reason (and return an error). The thing we have to do now is to make it sort that last slice he just ignores. I'll only copy the mergesort-function as it is the only thing that will change:

void mergesort(int key[], int n) //key is the array, n is the size of key
{
    int j, k, neglected, *w;
    w = calloc(n, sizeof(int));
    assert(w != NULL);

    for(k = 1; k < n; k *= 2){
        for(j = 0; j <= n - (k*2); j += 2 * k){
            merge(key + j, key + j + k, w + j, k, k);
        }

        //size of part that got neglected (if it could fully be divided in slices of 2*k, this will be 0)
        neglected = n % (2*k);

        //copy everything except the neglected part (if there was none, it will copy everything)
        for(j = 0; j < n-neglected; ++j) {
            key[j] = w[j];
        }

        if(neglected != 0 && neglected < n){ //couldn't devide it fully in slices of 2*k ==> the last elements were left out! merge them together with the last merged slice 
            merge(key + n - (2*k) - neglected, key + n-neglected, w + n - (2*k) - neglected, 2*k, neglected);
            for(j = n - (2*k) - neglected; j < n; ++j) { //copy the part we just merged
                key[j] = w[j];
            }
        }

        for(j = 0; j < n; ++j) {
            key[j] = w[j];
        }
    }
    free(w);
}

Also, my compiler was complaining about a variable you weren't using: m



来源:https://stackoverflow.com/questions/40796394/c-merge-sort-of-an-array-with-uneven-number-of-elements

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!