归并排序和快速排序

一:归并排序

要排序一个数组,我们先把数组分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就有序了。

归并排序使用的就是分治思想。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。分治思想一般是通过递归来实现的。

代码实现:

/**
 * 递归切分待排
 *
 * @param nums  待切分数组
 * @param left  待切分最后第一个元素的索引
 * @param right 待切分数组最后一个元素的索引
 */
public static void mergeSort(int[] nums, int left, int right) {
    if (left < right) {
        // 找出中间索引
        int center = (left + right) / 2;
        // 对左边数组进行递归
        mergeSort(nums, left, center);
        // 对右边数组进行递归
        mergeSort(nums, center + 1, right);
        // 合并
        merge(nums, left, center, right);
    }
}

/**
 * 两两归并排好序的数组(2路归并)
 * @param nums   带排序数组对象
 * @param left   左边数组的第一个索引
 * @param center 左数组的最后一个索引,center + 1右数组的第一个索引
 * @param right  右数组的最后一个索引
 */
private static void merge(int[] nums, int left, int center, int right) {
    int[] tmpArray = new int[right - left + 1];
    int leftIndex = left;   //左数组第一个元素的索引
    int rightIndex = center + 1;   //右数组第一个元素索引
    int tmpIndex = 0;    //临时数组索引

    // 把较小的数先移到新数组中
    while (leftIndex <= center && rightIndex <= right) {
        if (nums[leftIndex] <= nums[rightIndex]) {
            tmpArray[tmpIndex++] = nums[leftIndex++];
        } else {
            tmpArray[tmpIndex++] = nums[rightIndex++];
        }
    }

    // 把左边剩余的数移入数组
    while (leftIndex <= center) {
        tmpArray[tmpIndex++] = nums[leftIndex++];
    }

    // 把右边边剩余的数移入数组
    while (rightIndex <= right) {
        tmpArray[tmpIndex++] = nums[rightIndex++];
    }

    
    //可以优化成下面的写法
    System.arraycopy(tmpArray, 0, nums, left, tmpArray.length);
    System.out.println("nums: " + Arrays.toString(nums));
}

运行结果:

时间复杂度:归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管最好情况、最坏情况、平均情况,时间复杂度都是O(nlogn)

空间复杂度:归并排序有一个致命弱点,不是原地排序算法。在归并排序的合并两个有序数组的函数中,需要借助额外的存储空间。但是在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过n个数据的大小,所以空间复杂度是O(n)

 

二:快速排序

如果要排序数组中下标从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot(分区点)。

我们遍历p到r之间的数据,将小于pivot的数据放到左边,大于pivot的放到右边,将pivot放到中间。经过这一步骤之后,数组p到r之间的数据分成了三部分,前面p到q-1之间都是小于pivot的,中间是pivot,后面的q+1到r之间是大于pivot的。

归并排序有个合并merge函数,这里有个分区partition函数,就是随机选择一个元素(可以选择p到r区间的最后一个元素 ),然后对A[p....r]分区,函数返回pivot的下标。如果不考虑空间消耗的话,partition分区函数可以写的非常简单,申请两个临时数组X和Y,遍历A[p....r],将小于pivot的元素都拷贝到临时数组X,将大于pivot的元素都拷贝到临时数组Y,最后再将数组X和数组Y数据顺序拷贝到A[p...r]

但是这样partition就需要申请很多额外的内存空间,所以快排就不是原地排序算法了。如果希望是原地排序算法,空间复杂度得是O(1),那partition函数就不能占用太多额外的内存空间,我们就需要在A[p...r]的原地完成分区操作。

原地分区:

我们通过游标 i 把 A[p…r-1] 分成两部分。A[p…i-1] 的元素都是小于 pivot 的,我们暂且叫它“已处理区间”,A[i…r-1] 是“未处理区间”。我们每次都从未处理的区间 A[i…r-1] 中取一个元素 A[j],与 pivot 对比,如果小于 pivot,则 将其加入到已处理区间的尾部,也就是 A[i] 的位置。 数组的插入操作还记得吗?在数组某个位置插入元素,需要搬移数据,非常耗时。当时我们也讲了一种处理技巧,就是交换,在 O(1) 的时间复杂度内完成插入操作。这里我们也借助 这个思想,只需要将 A[i] 与 A[j] 交换,就可以在 O(1) 时间复杂度内将 A[j] 放到下标为 i的位置。

代码:

/**
 * 快速排序递归函数
 *
 * @param arr
 * @param p
 * @param r
 */
public static void quickSort(int[] arr, int p, int r) {
    if (p >= r) {
        return;
    }

    int q = partition(arr, p, r);
    quickSort(arr, p, q - 1);
    quickSort(arr, q + 1, r);
}

/**
 * 分区函数
 *
 * @param arr
 * @param p
 * @param r
 * @return 分区点位置
 */
private static int partition(int[] arr, int p, int r) {
    int pivot = arr[r];
    int i = p;
    for (int j = p; j < r; j++) {
        if (arr[j] <= pivot) {
            swap(arr, i, j);
            i++;
        }
    }

    swap(arr, i, r);
    System.out.println("arr: " + Arrays.toString(arr));
    System.out.println();
    return i;
}

/**
 * 交换
 *
 * @param arr
 * @param i
 * @param j
 */
private static void swap(int[] arr, int i, int j) {
    if (i == j) {
        return;
    }

    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

运行结果:

归并和快排的区别:

归并和快排用得都是分治思想,那他们的区别在哪里呢?可以发现归并排序的处理过程是由下到上,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题。归并排序虽然是稳定的。时间复杂度为O(nlogn)的排序算法,但是它是非原地排序算法,主要是合并函数无法在原地执行。快速排序通过设计巧妙的原地分区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。

快排性能分析:

如果每次分区操作,都能把数组分成大小接近相等的两个区间,那快排的时间复杂度递推求解公式跟归并是相同的。所以这种情况下时间复杂度为O(nlogn)。但是公式成立的前提是每次分区操作,我们选择的pivot都很合适,正好能将大区间对等的一分为二,实际上这种情况是很难实现的。

举一个比较极端的例子,如果数组中的数据原来就是有序的,比如:1,3,6,8,10,15,如果每次选择最后的元素作为pivot,那每次分区得到的两个区间是不均等,需要进行N次分区,才能完成整个的快排动作。每次分区大约要扫描n/2个元素,这种情况下,快排的时间复杂度就从O(nlogn)退化成了O(n2),但我们可以通过合理的选择pivot来避免这种情况。

归并排序快速排序都是常见的排序算法归并排序的基本思想是将两个及以上的有序表合并为一张有序表,通过分治法将待排序序列分为若干个有序子序列,然后再逐步合并这些子序列,最终得到一个有序序列。归并排序的时间复杂度为O(nlogn)。而快速排序的基本思想是通过分割和递归的方式将一个序列划分为较小的子序列,然后对这些子序列进行排序,最终得到一个有序序列。快速排序的时间复杂度为O(nlogn)。 在C语言中,可以使用递归的方式来实现归并排序快速排序。下面给出一个简单的代码示例: 归并排序的C语言代码示例: ```c #include <stdio.h> void merge(int arr[], int left, int mid, int right) { int i, j, k; int n1 = mid - left + 1; int n2 = right - mid; int L[n1], R[n2]; for (i = 0; i < n1; i++) L[i] = arr[left + i]; for (j = 0; j < n2; j++) R[j] = arr[mid + 1 + j]; i = 0; j = 0; k = left; while (i < n1 && j < n2) { if (L[i] <= R[j]) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } k++; } while (i < n1) { arr[k] = L[i]; i++; k++; } while (j < n2) { arr[k] = R[j]; j++; k++; } } void mergeSort(int arr[], int left, int right) { if (left < right) { int mid = left + (right - left) / 2; mergeSort(arr, left, mid); mergeSort(arr, mid + 1, right); merge(arr, left, mid, right); } } int main() { int arr[] = { 12, 11, 13, 5, 6, 7 }; int n = sizeof(arr) / sizeof(arr[0]); mergeSort(arr, 0, n - 1); printf("Sorted array: \n"); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); return 0; } ``` 快速排序的C语言代码示例: ```c #include <stdio.h> void swap(int* a, int* b) { int t = *a; *a = *b; *b = t; } int partition(int arr[], int low, int high) { int pivot = arr[high]; int i = (low - 1); for (int j = low; j <= high - 1; j++) { if (arr[j] < pivot) { i++; swap(&arr[i], &arr[j]); } } swap(&arr[i + 1], &arr[high]); return (i + 1); } void quickSort(int arr[], int low, int high) { if (low < high) { int pi = partition(arr, low, high); quickSort(arr, low, pi - 1); quickSort(arr, pi + 1, high); } } int main() { int arr[] = { 10, 7, 8, 9, 1, 5 }; int n = sizeof(arr) / sizeof(arr[0]); quickSort(arr, 0, n - 1); printf("Sorted array: \n"); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值