时间复杂度及稳定性
一、插入排序
将每一步待排序元素插入到已经完成排序的元素中,直到插完所有元素为止。
具体的代码实现:
public class InsertSort implements SortMethod{
@Override
public int[] sort(int[] nums) {
for (int i = 0; i < nums.length; i++) { //遍历每一个元素
for (int j = i; j > 0 ; j--) { // 与前面的元素相比较
if (nums[j]<nums[j-1]){
swap(nums,j,j-1);
}else { //已经有序了
break;
}
}
}
return nums;
}
}
复杂度分析:
时间复杂度为O(n2),空间复杂度为O(1),若元素越接近有序,则插入排序的时间效率越高。插入排序是一种稳定的排序算法
二、希尔排序
希尔排序也叫缩小增量排序,本质上是对插入排序的优化(利用插入排序当元素集合越接近有序,插入排序算法效率更高的特点)。
思想:
对待排序数组中的元素进行分组, 从第一个元素开始,按照数组下标中间隔为gap大小的元素分为一组,对每一组进行排序,重新选择gap的大小使得原始数据更加有序,当gap=1的时候就是插入排序。
public class ShellSort implements SortMethod{
@Override
public int[] sort(int[] nums) {
int gap = nums.length;
while (gap > 1){
gap = gap/3 +1; //调整gap的大小,当gap=1的时候,为插入排序
for (int i=gap;i<nums.length;i++){ // 总共只需要循环len-gap此
for (int j = i; j >=gap ; j-=gap) { // 插入排序
if (nums[j]<nums[j-gap]){
swap(nums,j,j-gap);
}else {
break;
}
}
}
}
return nums;
}
}
希尔排序是不稳定的,他的时间复杂度为O(nlog2n),空间复杂度为O(1)
三、选择排序
思想:每次选择数组元素中最小(最大)的元素放在序列的起始位置,直到全部待排序的数据元素排完。
public class SelectSort implements SortMethod{
@Override
public int[] sort(int[] nums) {
int len = nums.length;
for (int i = 0; i < len; i++) {
int location = i ;//记录最小值的位置
for (int j = i; j < len; j++) {
if (nums[location]>nums[j]){
location=j;
}
}
if (i!=location){
swap(nums,i,location);
}
}
return nums;
}
}
复杂度分析:O(n2),不稳定
四、堆排序
堆是一棵顺序存储的完全二叉树。
其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。
其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆。
举例来说,对于n个元素的序列{R0, R1, ... , Rn}当且仅当满足下列关系之一时,称之为堆:
Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)
Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)
其中i=1,2,…,n/2向下取整;
如上图所示,序列R{3, 8, 15, 31, 25}是一个典型的小根堆。
堆中有两个结点,元素3和元素8。
元素3在数组中以R[0]表示,它的左孩子结点是R[1],右孩子结点是R[2]。
元素8在数组中以R[1]表示,它的左孩子结点是R[3],右孩子结点是R[4],它的父结点是R[0]。可以看出,它们满足以下规律:
设当前元素在数组中以R[i] 表示,那么,
(1) 它的左孩子结点是:R[2*i+1] ;
(2) 它的右孩子结点是:R[2*i+2] ;
(3) 它的父结点是:R[(i-1)/2] ;
(4) R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。
首先,按堆的定义将数组R[0..n]调整为堆(这个过程称为创建初始堆),交换R[0]和R[n];
然后,将R[0..n-1]调整为堆,交换R[0]和R[n-1];
如此反复,直到交换了R[0]和R[1]为止。
以上思想可归纳为两个操作:
(1)根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大)。
(2)每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。
当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。
先通过详细的实例图来看一下,如何构建初始堆。
设有一个无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }。
构造了初始堆后,我们来看一下完整的堆排序处理:
还是针对前面提到的无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 } 来加以说明。
比如如下数组 {57, 40, 38, 11, 13, 34, 48, 75, 6, 19, 9, 7}堆排序前如下:
进行堆排序后如下:
最大堆的存储结构如下:
接着,最后一步,堆排序,进行(n-1)次循环。
相信,通过以上两幅图,应该能很直观的演示堆排序的操作处理。
看完上面所述的流程你至少有一个疑问:
如何确定最后一个非叶子结点?
其实这是有一个公式的,设二叉树结点总数为 n,则最后一个非叶子结点是第⌊n/2⌋个。
代码:
package com.example.demo.paixu;
import java.util.Arrays;
/**
* @Author: liyangjing
* @Date: 2022/03/21/11:08
* @Description:
*/
public class HeapSort {
public static void main(String[] args) {
//堆排序原理:堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
//大顶堆:arr[i]>=arr[i*2+1]&&arr[i]>=arr[i*2+2]
//小顶堆:arr[i]<=arr[i*2+1]&&arr[i]<=arr[i*2+2]
int[] arr = {4,21,5,86,74,12,57,42};
//求出最大的非叶子节点的索引
int startIndex=(arr.length-1)/2;
//循环开始调,最大的非叶子节点多大,则循环几次,从最大的非叶子节点开始调
for (int i = startIndex; i >=0; i--) {
toMaxheap(arr,arr.length,i);
}
//以上的运行结果已经达到大顶堆的效果,此时只需将根节点的元素与最后一个叶子节点的元素互换就行,并递归将剩余元素继续转化成大顶堆
for (int i = arr.length-1; i > 0; i--) {
int t=arr[i];
arr[i]=arr[0];
arr[0]=t;
//将剩余元素继续转化为大顶堆结构
toMaxheap(arr,i,0);
}
System.out.println(Arrays.toString(arr));
}
/**
*
* @param arr //要进行排序的数组
* @param size //要排序的范围
* @param startIndex //起始的索引位置
*/
private static void toMaxheap(int[] arr, int size, int startIndex) {
//求出左右节点的索引
int leftNodeIndex=startIndex*2+1;
int rightNodeIndex=startIndex*2+2;
//假设刚开始最大数的索引就是起始索引
int maxIndex=startIndex;
//求出最大节点所对应的索引
if(leftNodeIndex<size&&arr[leftNodeIndex]>arr[maxIndex]){
maxIndex=leftNodeIndex;
}
if(rightNodeIndex<size&&arr[rightNodeIndex]>arr[maxIndex]){
maxIndex=rightNodeIndex;
}
//调换位置,将最大节点放在大顶堆的根节点处
if(maxIndex!=startIndex){
int t=arr[startIndex];
arr[startIndex]=arr[maxIndex];
arr[maxIndex]=t;
//互换完之后可能会影响最大节点以下的大顶堆结构,所以这里需要递归调用方法,保证每个子树都是大顶堆结构
toMaxheap(arr,size,maxIndex);
}
}
}
五、冒泡排序
非常经典的排序算法。
思想:将两个元素两两进行排序,遍历完一次都会把最大(小)的元素放在了后面,是一种非常容易理解的排序方法。
public class BubbleSort implements SortMethod{
@Override
public int[] sort(int[] nums) {
int len = nums.length;
for (int i = 1; i < len; i++) {
for (int j = 0; j < len-i; j++) {
if (nums[j] > nums[j+1]){
swap(nums,j,j+1);
}
}
}
return nums;
}
}
复杂度分析:时间复杂度O(n2), 稳定
六、快速排序
快速排序属于交换排序的一种
1.单边循环
单边循环主要步骤:
- 找到基准点
- 循环遍历,发现比基准点小的则交换
- 分而治之,分治算法 递归,打印
public class QuickSort implements SortMethod{
@Override
public int[] sort(int[] nums) {
s(nums,0,nums.length-1);
return nums;
}
public void s(int[] numbers , int left ,int right){
if (left>=right){
return;
}
// 1.找到基准点
int pv = numbers[right];
// 2. 低点
int i = left;
// 3.循环遍历
for (int j = left; j <=right ; j++) {
if (numbers[j]<pv){
swap(numbers,j,i);
i++;
}
}
//中间元素 右边都比他大 ,左边都比他小
swap(numbers,i,right);
System.out.println(Arrays.toString(numbers));
s(numbers,left,i-1);
s(numbers,i+1,right);
}
}
2.双边循环
双边循环主要步骤:
- 找到基准点 左边
- 双指针比较 ,分别找到不符合要求的 进行交换。(先找到小的,在找到大的)
- 交换基准点值
- 分而治之
//双边循环
public void s1(int[] numbers ,int left ,int right){
if (left>=right){
return;
}
// 1.找到基准点
int pv = numbers[left];
// 保证左右点
int i = left;
int j = right;
while (i<j){
while (i<j && pv<numbers[j]){
j--;
}
while (i<j && pv >= numbers[i]){
i++;
}
swap(numbers,i,j);
}
swap(numbers,left,j);
System.out.println(Arrays.toString(numbers));
s1(numbers,left,j-1);
s1(numbers,j+1,right);
}
时间复杂度: O(nlogn) 最坏情况O(n2) 他是不稳定的排序算法
七、归并排序
归并算法采用非常经典的分治策略,每次把序列分成n/2的长度,将问题分解成小问题,由复杂变简单。
复杂度:时间复杂度O(NlogN),空间复杂度O(N)。
稳定性:稳定
/**
* 归并排序
* 简介:将两个(或两个以上)有序表合并成一个新的有序表 即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列
* 时间复杂度为O(nlogn)
* 稳定排序方式
* @param nums 待排序数组
* @return 输出有序数组
*/
public static int[] sort(int[] nums, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左边
sort(nums, low, mid);
// 右边
sort(nums, mid + 1, high);
// 左右归并
merge(nums, low, mid, high);
}
return nums;
}
/**
* 将数组中low到high位置的数进行排序
* @param nums 待排序数组
* @param low 待排的开始位置
* @param mid 待排中间位置
* @param high 待排结束位置
*/
public static void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
复杂度:时间复杂度O(NlogN),空间复杂度O(N)。
稳定性:稳定