排序算法-O(nlog)排序-快速排序、归并排序、堆排序(java实现)


活动地址:CSDN21天学习挑战赛

学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您:
想系统/深入学习某技术知识点…
一个人摸索学习很难坚持,想组团高效学习…
想写博客但无从下手,急需写作干货注入能量…
热爱写作,愿意让自己成为更好的人…

1.堆排序

通过建立一个大顶堆,依次取出大顶堆元素放到末尾,再次建堆(分治)

  • 排序函数:void sort(int[] nums)
  • 恢复函数:void siftDown(int[] nums, int parent, int heapSize)
1.1 算法分析

选择排序可以看作是冒泡排序的优化,而堆排序则可以看作是选择排序的优化,其减少了选择的次数,步骤如下:

首先对序列进行原地建堆(heapify)然后重复执行以下操作,直到堆的元素数量为 1

  1. 交换堆顶元素与尾元素
  2. 堆的元素数量减 1
  3. 对 0 位置进行 1 次 siftDown(下滤) 操作
1.2 算法实现
public void sort(int[] nums) {
    // 建堆(自低向上,下滤)
    int heapSize = nums.length;
    for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
        siftDown(nums, i, heapSize);
    }
    // 依次取出堆顶,放到数组尾部
    while (heapSize > 1) {
        heapSize--;
        int tmp = nums[heapSize];
        nums[heapSize] = nums[0];
        nums[0] = tmp;
        siftDown(nums, 0, heapSize);
    }
}


public void siftDown(int[] nums, int parent, int heapSize) {
    int element = nums[parent];
    // 最后一个非叶子下标
    int end = (heapSize >> 1) - 1;
    while (parent <= end) {
        // 寻找最大孩子元素
        int childI = parent * 2 + 1;
        int childV = nums[childI];
        if (childI + 1 < heapSize && nums[childI + 1] > nums[childI]) {
            childI = childI + 1;
            childV = nums[childI];
        }
        // 如果最大孩子大于父元素,则将孩子上移,否则结束循环
        if (childV > element) {
            nums[parent] = childV;
            parent = childI;
        } else {
            break;
        }
    }
    nums[parent] = element;
}
1.3 复杂度分析
  • 时间复杂度:首先建堆的时间复杂度为O(n),其次需要进行大约n次的时间复杂度为O(1)的获取堆顶元素操作,而每次获取堆顶元素后都需要进行下滤操作,而下滤操作的时间复杂度为O(logn),所以总体的时间复杂度为O(n) + O(n) * (O(1) + O(logn)) = O(nlogn)
  • 空间复杂度:没有引入新的空间,直接将选出的最值放到原先数组的末尾,所以为O(1)
1.4 细节分析

上面的建堆采用的是自下而上的下滤方式建堆,除此之外,其实还有一种自上而下的上滤建堆方式,只不过其建堆的时间复杂度会由原先的O(n)上升到O(nlogn),所以没有采用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x5gVeu6j-1660461290931)(img/heapify.png)]

2.快速排序

通过选取的某个数,将数据分成两部分,一部分小于这个数,一部分大于这个数,直到不能分为止(分治)

排序函数:void sort(int[] nums)

快速排序:void sort(int[] nums, int left, int right)

返回轴点:int pivotIndex(int[] nums, int left, int right)

2.1 算法分析

快速排序可以说是我们最常用的一种排序算法了,其运用了分治的思想,平均每次减少一半的复杂度。首选,快速排序会选出一个数作为轴点,可以选择数组的最左边元素,也可以随机的选择一个元素,这里推荐随机的选择一个元素,因为如果使用最左边元素作为轴点可能出现最差情况(数组原先是完全正/逆序排列)。

快速排序的核心就是确定轴点元素的位置,上面我们已经随机获取到了一个轴点值。首先,将其与最左边元素交换位置,然后保存轴点元素值,之后从右开始与这个轴点元素进行比较,如果比这个轴点元素大,则继续向左移动,如果比这个轴点元素小,则将此元素覆盖轴点元素并开始在从左边向右扫描,如果比轴点元素小,则继续向左移动,如果比轴点元素大,则将值覆盖上次右侧的指针指向的元素,直到左右指针重合,则该点就是轴点元素的位置。

2.2 算法实现
public void sort(int[] nums) {
    sort(nums, 0, nums.length);
}
/**
 * [left,right)的快速排序
 */
private void sort(int[] nums, int left, int right) {
    if (right - left < 2) return;
    int pivotIndex = pivotIndex(nums, left, right);
    sort(nums, left, pivotIndex);
    sort(nums, pivotIndex + 1, right);
}
/**
 * 快排核心操作,返回轴点位置
 */
private int pivotIndex(int[] nums, int left, int right) {
    // 选取轴点,如果为 pivot[left]则容易分摊不均
    E pivot = nums[left + new Random().nextInt(right - left)];
    // 将右指针指向右边最后一个元素
    right--;
    while (left < right) {
        // 从右边向左扫描
        while (left < right) {
            if (pivot < nums[right]) {
                right--;
            } else {
                nums[left++] = nums[right];
                break;
            }
        }
        // 从左边开始向右扫描
        while (left < right) {
            if (pivot > nums[left]) {
                left++;
            } else {
                nums[right--] = nums[left];
                break;
            }
        }
    }
    nums[left] = pivot;
    return left;
}
2.3 复杂度分析
  • 时间复杂度:最好(平均)时间复杂度O(nlog),最坏时间复杂度O(n^2)
  • 空间复杂度:没有新的额外空间引入O(1)

3.归并排序

归并排序,类似于快速排序,只不过其每次都是均匀的从中间分割元素

排序函数:void sort(int[] nums)

递归排序:void sort(int[] nums, int left, int right)

合并操作:merge(int begin, int endLeft, int endRight)

3.1 算法分析

归并排序,是将一个大数组拆分为两个小数组,使两个小数组有序后在进行组合,这样每次递归都会减少一半的数据量

3.2 算法实现
protected void sort(int[] nums) {
    sort(nums,0, data.length);
}

/**
 * 从[left,right)排序
 */
private void sort(int[] nums, int left, int right) {
    if (right - left < 2) return;
    int mid = (left + right) >> 1;
    sort(left, mid);
    sort(mid, right);
    merge(left, mid, right);
}

/**
 * 左侧[left, mid),右侧[mid,endRight)
 */
private void merge(int[] nums, int begin, int endLeft, int endRight) {
    // 创建临时数组,用于copy
    E[] tmp = (E[]) new Object[endRight - begin];
    int curIndex = 0;
    // 左右数组指针
    int p1 = begin;
    int p2 = endLeft;
    while (p1 < endLeft && p2 < endRight) {
        // 为保证稳定性,一定要为 > 不能是 >=
        if (data[p1] > data[p2]) {
            tmp[curIndex++] = nums[p2++];
        } else {
            tmp[curIndex++] = nums[p1++];
        }
    }
    // 处理剩余元素
    while (p1 < endLeft) {
        tmp[curIndex++] = nums[p1++];
    }
    while (p2 < endRight) {
        tmp[curIndex++] = nums[p2++];
    }
    System.arraycopy(tmp, 0, data, begin, endRight - begin);
}
3.3 时间复杂度分析
  • 时间复杂度:由于每次都是均分,所有时间复杂度为O(nlogn)
  • 空间复杂度:需要n/2的空间存储临时数据,所以空间复杂度为o(n)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值