【数据结构和算法】排序算法(二)

希尔排序

  • 对插入排序的改进,也称缩小增量排序
  • 分组(length/2),对每组使用直接插入排序
  • 移位法和交换法

在这里插入图片描述

// 希尔排序时, 对有序序列在插入时采用交换法
public static void shellSort(int[] arr) {
    
    

    int temp = 0;
    int count = 0;
    // 根据前面的逐步分析,使用循环处理
    for (int gap = arr.length / 2; gap > 0; gap /= 2) {
    
    
        for (int i = gap; i < arr.length; i++) {
    
    
            // 遍历各组中所有的元素(共gap组,每组有个元素), 步长gap
            for (int j = i - gap; j >= 0; j -= gap) {
    
    
                // 如果当前元素大于加上步长后的那个元素,说明交换
                if (arr[j] > arr[j + gap]) {
    
    
                    temp = arr[j];
                    arr[j] = arr[j + gap];
                    arr[j + gap] = temp;
                }
            }
        }
    }
}
//对交换式的希尔排序进行优化->移位法
public static void shellSort2(int[] arr) {
    
    

    // 增量gap, 并逐步的缩小增量
    for (int gap = arr.length / 2; gap > 0; gap /= 2) {
    
    
        // 从第gap个元素,逐个对其所在的组进行直接插入排序
        for (int i = gap; i < arr.length; i++) {
    
    
            int j = i;
            int temp = arr[j];
            if (arr[j] < arr[j - gap]) {
    
    
                while (j - gap >= 0 && temp < arr[j - gap]) {
    
    
                    //移动
                    arr[j] = arr[j-gap];
                    j -= gap;
                }
                //当退出while后,就给temp找到插入的位置
                arr[j] = temp;
            }
        }
    }
}

归并算法

  • 分治策略

在这里插入图片描述

在这里插入图片描述

//分+合方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
    
    
    if(left < right) {
    
    
        int mid = (left + right) / 2; //中间索引
        //看似是分解其实都数都在arr中 只是 下标在分解
        //向左递归进行分解
        mergeSort(arr, left, mid, temp);
        //向右递归进行分解
        mergeSort(arr, mid + 1, right, temp);
        //递归到每一个数分开时,再将每个数一次合并再合并
        //比如8个数,先将12,34,56,78依次合并 再将1234,5678依次合并。。。。0
        //每次合并后都会把合并好的数从temp拷贝到arr中,合并多少就拷贝多少
        merge(arr, left, mid, right, temp);

    }
}

//合并的方法
/**
	 * 
	 * @param arr 排序的原始数组
	 * @param left 左边有序序列的初始索引
	 * @param mid 中间索引
	 * @param right 右边索引
	 * @param temp 做中转的数组
	 */
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
    
    

    int i = left; // 初始化i, 左边有序序列的初始索引
    int j = mid + 1; //初始化j, 右边有序序列的初始索引
    int t = 0; // 指向temp数组的当前索引

    //(一)
    //先把左右两边(有序)的数据按照规则填充到temp数组
    //直到左右两边的有序序列,有一边处理完毕为止
    while (i <= mid && j <= right) {
    
    //继续
        //如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
        //即将左边的当前元素,填充到 temp数组 
        //然后 t++, i++
        if(arr[i] <= arr[j]) {
    
    
            temp[t] = arr[i];
            t += 1;
            i += 1;
        } else {
    
     //反之,将右边有序序列的当前元素,填充到temp数组
            temp[t] = arr[j];
            t += 1;
            j += 1;
        }
    }

    //(二)
    //把有剩余数据的一边的数据依次全部填充到temp
    while( i <= mid) {
    
     //左边的有序序列还有剩余的元素,就全部填充到temp
        temp[t] = arr[i];
        t += 1;
        i += 1;	
    }

    while( j <= right) {
    
     //右边的有序序列还有剩余的元素,就全部填充到temp
        temp[t] = arr[j];
        t += 1;
        j += 1;	
    }

    //(三)
    //将temp数组的元素拷贝到arr
    //注意,并不是每次都拷贝所有
    t = 0;
    int tempLeft = left; // 
    //第一次合并 tempLeft = 0 , right = 1 //  tempLeft = 2  right = 3 // tL=0 ri=3
    //最后一次 tempLeft = 0  right = 7
    while(tempLeft <= right) {
    
     
        arr[tempLeft] = temp[t];
        t += 1;
        tempLeft += 1;
    }

}

基数排序

  • 桶排序,将整数拆分来比较

在这里插入图片描述

在这里插入图片描述

//基数排序方法
public static void radixSort(int[] arr) {
    
    

    //根据前面的推导过程,我们可以得到最终的基数排序代码

    //1. 得到数组中最大的数的位数
    int max = arr[0]; //假设第一数就是最大数
    for(int i = 1; i < arr.length; i++) {
    
    
        if (arr[i] > max) {
    
    
            max = arr[i];
        }
    }
    //得到最大数是几位数
    int maxLength = (max + "").length();


    //定义一个二维数组,表示10个桶, 每个桶就是一个一维数组
    //说明
    //1. 二维数组包含10个一维数组
    //2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
    //3. 名明确,基数排序是使用空间换时间的经典算法
    int[][] bucket = new int[10][arr.length];

    //为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
    //可以这里理解
    //比如:bucketElementCounts[0] , 记录的就是  bucket[0] 桶的放入数据个数
    int[] bucketElementCounts = new int[10];


    //这里我们使用循环将代码处理

    for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) {
    
    
        //(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位..
        for(int j = 0; j < arr.length; j++) {
    
    
            //取出每个元素的对应位的值
            int digitOfElement = arr[j] / n % 10;
            //放入到对应的桶中
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
            bucketElementCounts[digitOfElement]++;
        }
        //按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
        int index = 0;
        //遍历每一桶,并将桶中是数据,放入到原数组
        for(int k = 0; k < bucketElementCounts.length; k++) {
    
    
            //如果桶中,有数据,我们才放入到原数组
            if(bucketElementCounts[k] != 0) {
    
    
                //循环该桶即第k个桶(即第k个一维数组), 放入
                for(int l = 0; l < bucketElementCounts[k]; l++) {
    
    
                    //取出元素放入到arr
                    arr[index++] = bucket[k][l];
                }
            }
            //第i+1轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
            bucketElementCounts[k] = 0;

        }
        //System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));

    }
}

堆排序

时间复杂度O(nlogn) 将一个数组看成二叉树来比较,顺序存储

大顶堆:每个结点的值都大于等于左右孩子的值

在这里插入图片描述

小顶堆:每个结点的值都小于等于左右孩子的值

在这里插入图片描述

  • 将一个待排序序列构成一个大顶堆(大小顶堆取决于升降序)
  • 将根结点与末尾交换,然后再将n-1个数构成大顶堆

构成大顶堆

  • 先从最后一个给叶子结点开始arr.lrngth/2-1= 5/2 = 1

在这里插入图片描述

交换

在这里插入图片描述

继续满足大顶堆

在这里插入图片描述

//编写一个堆排序的方法
public static void heapSort(int arr[]) {
    
    
    int temp = 0;
    System.out.println("堆排序!!");
    for(int i = arr.length / 2 -1; i >=0; i--) {
    
    
        adjustHeap(arr, i, arr.length);
    } 
    /*
		 * 2).将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  			3).重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
		 */
    for(int j = arr.length-1;j >0; j--) {
    
    
        //交换
        temp = arr[j];
        arr[j] = arr[0];
        arr[0] = temp;
        adjustHeap(arr, 0, j); 
    }
}

//将一个数组(二叉树), 调整成一个大顶堆
/**
	 * @param arr 待调整的数组
	 * @param i 表示非叶子结点在数组中索引
	 * @param lenght 表示对多少个元素继续调整, length 是在逐渐的减少
	 * 功能: 完成 将 以 i 对应的非叶子结点的树调整成大顶堆
	 * 举例  int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => 得到 {4, 9, 8, 5, 6}
	 * 如果我们再次调用  adjustHeap 传入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5, 4}
	 */
public  static void adjustHeap(int arr[], int i, int lenght) {
    
    

    int temp = arr[i];//先取出当前元素的值,保存在临时变量
    //开始调整
    //说明
    //1. k = i * 2 + 1 k 是 i结点的左子结点
    for(int k = i * 2 + 1; k < lenght; k = k * 2 + 1) {
    
    
        if(k+1 < lenght && arr[k] < arr[k+1]) {
    
     //说明左子结点的值小于右子结点的值
            k++; // k 指向右子结点
        }
        if(arr[k] > temp) {
    
     //如果子结点大于父结点
            arr[i] = arr[k]; //把较大的值赋给当前结点
            i = k; //!!! i 指向 k,继续循环比较
        } else {
    
    
            break;//! 只需要调整 以i为结点的左右子结点,那为什么还要循环呢?
            //因为后面我们在执行-最大元素"沉"到数组末端的-时候 需要将最小的那个值放到根结点 所以需要循环又把它放到它该有的位置
            //这里肯定是i=0开始,因为他已经是一个大顶堆了,所以可以循环将它放入该有的位置
            //比如 {4,3,5,8,9} 我们以i=0开始发现会排序成=>{5,3,4,8,9}原因就是他不是大顶堆,所以break、
        }
    }
    //当for 循环结束后,我们已经将以i 为父结点的树的最大值,放在了 最顶(局部)
    arr[i] = temp;//将temp值放到调整后的位置
}

常用排序算法总结和对比

在这里插入图片描述

相关术语解释

  • 内排序:所有排序操作都在内存中完成
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过和内存的数据传输才能进行
  • 时间复杂度:一个算法执行所耗费的时间
  • 空间复杂度:运行完一个程序所需内存的大小
  • n:数据规模
  • k:“桶”的个数
  • In-palce:不占用额外内存
  • Out-place:占用额外内存
  • 稳定 :如果a在b的前面,而且a=b,排序之后a仍在b的前面
  • 不稳定:如果a在b的前面,而且a=b,排序之后a可能会出现在b的后面

笔记来源:B站 尚硅谷 韩老师

Guess you like

Origin blog.csdn.net/qq_51998352/article/details/121047640