数据结构与算法-排序算法

数据结构与算法之排序算法

    • 1 使用递归的方式求最大值
    • 2 时间复杂度
    • 3 选择排序和冒泡排序的时间复杂度分析
    • 4 插入排序时间复杂度分析 (O(N^2))
    • 5 二分法的详解与扩展
    • 6 对数器
    • 7 归并排序
    • 8 归并排序的应用
    • 9 逆序对问题
    • 10 归并排序非递归方式
    • 11 快速排序
    • 12 堆排序
    • 13 桶排序

1 使用递归的方式求最大值

public class GetMaxTest {
    public static int getMax(int[] arr){
        return process(arr,0,arr.length);
    }

    // arr[L..R]范围上的最大值
    public static int process(int[] arr,int L,int R){
        // arr[L..R]范围上只有一个数 直接返回 base case
        if (L == R) {
           return arr[L];
        }
        // 中点 
        // 如果采用这种写法
        // int mid = (L + R) / 2 
        // 存在的问题是 如果 L 和 R 比较大的时候可能会出现溢出的情况 
        int mid = L + ((R - L) >> 1);
        int leftMax = process(arr,L,mid);
        int rightMax = process(arr,mid + 1 ,R);
        return Math.max(leftMax,rightMax);// 时间复杂度 O(1)
    }
	public static void main(String[] args) {
	       int[] arr = {10,20,35,6,8};
	       System.out.println(getMax(arr));
	   }
}

递归将整个函数的调用过程 调用过程
数据结构与算法-排序算法_第1张图片

类似二叉树的后续遍历

递归行为和递归行为时间复杂度的估算
master公式 :
T(N) = a * T (N/b) + O(N^d)
T(N) : 母问题的规模
a : 调用次数
b : 子问题的规模

O(N^d): 除了子问题之外的时间复杂度

在此问题中T(N)公式为 :
a = 2 b = 2 d=0
T(N) = 2 * T(N/2) + O(1)

中间加一个打印 额外的时间复杂度就变成了 O(N)
log(b,a) > d 复杂度为O(N^log(b,a))
log(b,a) = d 复杂度为O(N^d*logN)
log(b,a) < d 复杂度为O(N^d)
此递归的时间复杂度为O(N)

2 时间复杂度

  • 常数时间的操作
    一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作
    时间复杂度作为一个算法流程中,常数操作数量的一个指标,常用O来表示,具体来说,需要对一个算法的流程非常熟悉,然后去写这个算法的流程中,发生了多少常数操作,从而总结出常数操作数量的表达式,
    在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为 f(N) 那么时间复杂度为O(f(N))

评价一个算法流程的好坏,先看时间复杂度指标,然后再分析不同样本下的实际运行时间,也就是"常数项时间"

3 选择排序和冒泡排序的时间复杂度分析

选择排序
coding

public class SelectionSortTest {
    public static void selectionSort(int[] arr){
        if (arr == null || arr.length < 2){
            return;
        }
        // 保证 i - N-1 上的数最小
        for (int i = 0; i < arr.length - 1 ; i++) {
            int minIndex = i;
            // 在 i   - (N - 1)上找最小值的下标
            for (int j = i + 1; j < arr.length;j++){
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            // 可能会出现 i == index
            swap(arr,i,minIndex);
        }
    }

    public static void swap(int[] arr,int i,int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
        // 使用下面这种交换方法 i 和 j 不能相等
       /* arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];*/
    }
}

冒泡排序
coding

public class BubbleSortTest {
    public static void bubbleSort(int[] arr){
        if (arr == null || arr.length < 2){
            return;
        }
        // 0 - (N - 1) 范围上相邻两个数交换 放在最后  时间复杂度为 O(N^2)
        for (int  e = arr.length - 1; e > 0; e--){
            // 从0开始,前一个数比后一个数大  就交换
            for (int j = 0;j < e;j++){
                if (arr[j] > arr[j + 1]){
                    swap(arr,j,j+1);
                }
            }
        }
    }
    
    // 可以交换的前提是 i != j 否则会将值修改成 0
    public static void swap(int[] arr,int i,int j){
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }
    
}

异或运算

相同为0,不同为1

异或运算的性质
0 ^ N = N
N ^ N = 0

异或运算满足交换律和结合律
a ^ b ^ c = a ^ c ^ b

异或运算可以理解为无进位相加

交换两个数使用异或的方法

// 自己跟自己异或是 0
// 自己跟 0 异或是自己 
// 交换的前提是在内存中是两块不同的区域
int a = 10;
int b = 20;
a = a ^ b;
b = a ^ b;// a^b^b a^0 a
a = a ^ b; // a = a ^ a ^ b 0 ^ b b

关于异或运算的算法
1 已知在一个数组中,只有一个数出现了奇数次,其它数都出了偶数次,如何找到这个数
coding

public static int findOccurOddTimes1(int[] arr) {
      int eor = 0;
      for (int ele : arr) {
          eor ^= ele;
      }
      return eor;
  }

2 一个数组中,有两个数字出现了奇数次 这两个数字不相同 其他数都是出现了偶数次 如何找出这两个数字

/**
     * 一个数组中,有两个数字出现了奇数次 这两个数字不相同 其他数都是出现了偶数次 如何找出这两个数字
     */
    public static int[] findOccurOddTimes2(int[] arr) {
        int[] retArr = new int[2];
        int eor = 0;

        for (int i = 0; i < arr.length; ++i) {
            eor ^= arr[i];
        }

        // eor 就是 a ^ b 因为 a和 b 不相等 所以 eor一定不是0 一定有一位是1
        // a 和 b 一定有一位上 a 是 0 b 是 1  所有的这些数可以分成这一位上是0和这一位上不是 0
        // 提取出数据最右边的 1 一个数字 与 它按位取反+1 就会提取出最右侧的1
        int rightNumber = eor & (~eor + 1);
        int onlyOne = 0;
        for (int cur : arr) {
            // 数组中所有这一位上是0的数与 onlyOne作与运算 最终得到的结果就是 A或B中的其中一个
            if ((cur & rightNumber) == 0) {
                onlyOne ^= cur;
            }
        }
        retArr[0] = onlyOne;
        retArr[1] = onlyOne ^ eor;
        return retArr;
    }

leecode类似题目

一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只
出现一次的数字。 
数据范围:数组长度 ,数组中每个数的大小 要求:空间复杂度 ,时间复杂度 提示:输出时按非降序排列。
提示:输出时按非降序排列。
import java.util.*;

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型一维数组
     */
    public int[] FindNumsAppearOnce (int[] nums) {
        // write code here
        if (nums == null || nums.length < 2){
            return null;
        }
        int eor = 0;
        for (int i = 0;i < nums.length;++i){
            eor ^= nums[i];
        }
        int rightNumber = eor & (~eor + 1);
        int onlyOne = 0;
        for(int i= 0;i < nums.length;++i){
            if((nums[i] & rightNumber) == 0){
                onlyOne ^= nums[i];
            }
        }
        int otherOne = (eor ^ onlyOne);
        int maxValue = onlyOne > otherOne ? onlyOne : otherOne; 
        int minValue = onlyOne > otherOne ? otherOne : onlyOne; 
        return new int[]{minValue,maxValue};
    }
}

4 插入排序时间复杂度分析 (O(N^2))

coding
插入排序在某些数据状况下,时间复杂度比选择排序和冒泡排序要好

从0开始 依次保证从 0 - (N -1)范围上有序

public class InsertSortTest {
    public static void  insertSort(int[] arr){
        if (arr == null || arr.length < 2){
            return;
        }
        // 0 - 0有序
        // 0 - i有序
        for (int i = 1; i < arr.length ; i++) {
        	// j 位置的数 比 j + 1 位置上的数大 就交换
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                swap(arr,j,j + 1);
            }
        }
    }

    public static void swap(int[] arr,int i,int j){
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }
}

5 二分法的详解与扩展

1 在一个有序数组中找某个数是否存在

public class BinarySearchTest {
    public static void main(String[] args) {
        int[] arr = {9,12,14,17,28,35,49};
        System.out.println(isExists(arr,14));
    }
    // 二分查找
    public static boolean isExists(int[] arr,int num){
        if (arr == null || arr.length == 0){
            return false;
        }
        int L = 0;
        int R = arr.length - 1;
        // 第一个位置和最后一个位置就是  直接返回
        if (arr[L] == num || arr[R] == num){
            return true;
        }
        // 不是 才开始二分查找
        while ( L < R) {
           int mid = L + ((R -L) >> 1);
           if (arr[mid] < num){
                // 往右半查找
               L = mid + 1;
           } else if (arr[mid] > num){
               //往左半查找
               R = mid - 1;
           } else {
               return true;
           }
        }
        return arr[L] == num;
    }
}

2 在一个数组中,找一个数最左侧的位置 同样可以使用二分法查找

/**
     *  在一个有序数组中,找>=某个数最左侧的位置
     * @param arr
     * @param X
     * @return
     */
    public static int searchLeftValue(int[] arr,int X){
        if (arr == null || arr.length == 0){
            return -1;
        }
        int L = 0;
        int R = arr.length - 1;
        int index = -1;
        while ( L < R){
            int mid = L + ((R - L) >> 1);
            if (arr[mid] < X) { // 小于的时候就是往右找
                L = mid + 1;
            } else if (arr[mid] >= X){ // 打到中间位置  往左找
               index = mid;
               R = mid - 1;
            }
        }
        return index;
    }

leetcode题目

请实现无重复数字的升序数组的二分查找
给定一个 元素升序的、无重复数字的整型数组 nums 和一个目标值 target ,
写一个函数搜索 nums 中的 target,如果目标值存在返回下标(下标从 0 开始),否则返回 -1
import java.util.*;
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @param target int整型 
     * @return int整型
     */
    public int search (int[] nums, int target) {
        // write code here
        if(nums == null || nums.length == 0){
            return -1;
        }

        if(nums[0] == target){
            return 0;
        }

        if(nums[nums.length - 1] == target){
            return nums.length - 1;
        }
        int index = -1;
        int leftIndex = 0;
        int rightIndex = nums.length - 1;
        while(leftIndex < rightIndex){
            int mid = leftIndex + ((rightIndex - leftIndex) >> 1);
            if(nums[mid] < target){
                leftIndex = mid + 1;
            } else if (nums[mid] > target) {
                rightIndex = mid - 1;
            } else {
                return mid; 
            }
        }
        if(nums[leftIndex] == target){
            index = leftIndex;
        }
        return index;
    }
}

3 在一个有序数组中,找<= 某个数最右侧的位置
coding

/**
     * 在一个有序数组中,找<= 某个数最右侧的位置
     * @param arr
     * @param X
     * @return
     */
    public static int getRightValue(int[] arr,int X){
        if (arr == null || arr.length == 0){
            return -1;
        }
        int L = 0;
        int R = arr.length - 1;
        int index = -1;
        while (L < R) {
            int M = L + ((R - L) >> 1);
            // 中间位置的数小于等于X 往右继续找
            if (arr[M] <= X) {
                index = M;
                L = M + 1;
            } else{
              R = M - 1;
            }
        }
        return index;
    }

4 局部最小问题 (数组无序,相邻的两个数一定不相等)
coding

/**
     *  局部最小值问题:
     * 无序数组,任意两个相邻的数不等,找到存在局部最小的位置
     * 0位置比1位置小,则0位置是局部最小,n-2位置比n-1位置小,返回n-1位置
     * 中间位置i,需满足 i 比左边小也比右边小,则i位置是局部最小
     * 局部最小位置存在即可返回,不用返回所有的位置
     * @param arr
     * @return
     */
    public static int getLessIndex(int[] arr){
        if (arr == null || arr.length == 0){
            return -1;
        }
        if (arr.length == 1 || arr[0] < arr[1]){
            return 0;
        }
        if (arr[arr.length -1 ] < arr[arr.length -2 ]){
            return arr.length -1 ;
        }
        int L = 1;
        int R = arr.length - 2;
        while (L < R){
            int M = L + ((R - L) >> 1);
            // 中点位置的数比后一个数要大 往右查找
            if (arr[M] > arr[M+1]){
                L = M + 1;
            } else if (arr[M] > arr[M-1]){ // 中点位置的数比前一个数要大 往左查找
                R = M -1;
            } else { // 中点位置的数小于前一个 也小于后一个 直接返回
                return M;
            }
        }
        return L;
    }

leetcode 类似题目

给定一个长度为n的数组nums,请你找到峰值并返回其索引。数组可能包含多个峰值,
在这种情况下,返回任何一个所在位置即可。
1.峰值元素是指其值严格大于左右相邻值的元素。严格大于即不能有等于
2.假设 nums[-1] = nums[n] = -∞
3.对于所有有效的 i 都有 nums[i] != nums[i + 1]
4.你可以使用O(logN)的时间复杂度实现此问题吗?

coding

import java.util.*;
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 此问题实际上是局部最大问题
     * @param nums int整型一维数组 
     * @return int整型
     */
    public int findPeakElement (int[] nums) {
        // write code here
        if(nums == null || nums.length == 0){
            return -1;
        }
        if(nums.length == 1 || nums[0] > nums[1]){
            return 0;
        }
        if(nums[nums.length - 1] > nums[nums.length - 2]){
            return nums.length -1;
        }
        int L = 1;
        int R = nums.length - 2;
        while(L < R){
            int M = L + ((R - L) >> 1);
            // 当前数比后一个要小 往右半查找
            if(nums[M] < nums[M + 1]){
                L = M + 1;
             // 中间的数比前一个数大 则往左半查找   
            } else if (nums[M] < nums[M - 1] ){
                R = M - 1;
             // 中间的数比后一个大且比前一个大 则当前数就是最大的
            } else {
                return M;
            }
        }
        return L;
    }
}

6 对数器

对数器的概念和使用

  1. 有一个你想要测试的方法A
  2. 实现复杂度不好但是容易实现的方法
  3. 实现一个随机样本产生器
  4. 把方法a和方法b跑相同的随机样本
  5. 如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法A或者方法B
  6. 当样本量很多时比对测试依然正确,可以确定方法A已经正确

coding

public static void test1() {
  int testTime = 5000000;
  boolean isSucceed = true;
  long startTime = System.currentTimeMillis();
  for (int i = 0; i < testTime; ++i) {
      int[] arr = genRandArr(100, 100);
      int[] cpyArr = copyArray(arr);
      Arrays.sort(cpyArr);
      selectSort(arr);
      if (!isEqual(arr, cpyArr)) {
          isSucceed = false;
          break;
      }
  }
  long endTimes = System.currentTimeMillis();
  System.out.println((endTimes - startTime));
  System.out.println(isSucceed ? "success" : "failed");
}


// 生成随机数
// Math.random() 返回 [0-1)之间的小数
public static int[] genRandArr(int maxSize, int maxValue) {
	// 0 <= Math.random() < 1.0 
    int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
    for (int i = 0; i < arr.length; ++i) {
        arr[i] = (int) ((maxValue + 1) * Math.random() - (maxValue + 1) * Math.random());
    }
    return arr;
}

public static int[] copyArray(int[] srcArr) {
    int[] destArr = new int[srcArr.length];
    System.arraycopy(srcArr, 0, destArr, 0, srcArr.length);
    return destArr;
}

public static boolean isEqual(int[] arr1, int[] arr2) {
    if (arr1.length != arr2.length) return false;
    for (int i = 0; i < arr1.length; i++) {
        if (arr1[i] != arr2[i]) return false;
    }
    return true;
}


/**
 * 选择排序
 *      每次选择出最小的数放在指定的位置
 * @param arr
 */
public static void selectSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[i]) {
                swap(arr, i, j);
            }
        }
    }
}

7 归并排序

整体就是一个简单递归,左边排好序、右边排好序、让其整体有序
让其整体有序的过程里用了外排序方法

public static void mergeSort(int arr[]){
         if (arr == null || arr.length < 2){
             return;
         }
         processSort(arr,0,arr.length-1);
 }

 public static void  processSort(int[] arr,int L,int R){
        if (R == L){
            return;
        }
        int M = L + ((R-L) >> 1);
        // 先让左边有序
        processSort(arr,L,M);
        // 再让右边有序
        processSort(arr,M+1,R);
        // 最后合并
        merge(arr,L,M,R);
 }

 // 将左边和右边合并 合并过程的时间复杂度是O(N)
 public static void merge(int[] arr,int L,int M,int R){
     // 创建一个数组 拷贝数据
     int[] tempArr = new int[R - L + 1];
     // 临时数组索引
     int index = 0;
     // 指向左边数组的下标
     int p1 = L;
     // 指向右边有序数组的下标
     int p2 = M+1;
     // 两边都不越界
     while(p1 <= M && p2 <= R) {
         tempArr[index++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
     }

     // 左边不越界
     while(p1 <= M){
         tempArr[index++] = arr[p1++];
     }

     // 右边不越界
     while (p2 <= R){
         tempArr[index++] = arr[p2++];
     }
	 // 将排好序的数据拷贝到原数组中		
     for (int i = 0;i<tempArr.length;++i){
         arr[L+i] = tempArr[i];
     }
 }

8 归并排序的应用

求小和问题
小和问题和逆序对问题
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组
的小和。求一个数组 的小和。
例子:[1,3,4,2,5]
1左边比1小的数,没有;
3左边比3小的数,1;
4左边比4小的数,1、3;
2左边比2小的数,1;
5左边比5小的数,1、3、4、2;
所以小和为1+1+3+1+1+3+4+2=16

小和问题可以转换成右侧有几个数比它大
时间复杂度为 O(N^2)

 // 时间复杂度 O(N^2)
    public static int vioMethod(int[] arr){
        int ret = 0;
        for (int i = 1;i < arr.length;++i){
            for(int j = 0;j < i;++j){
                if (arr[j] < arr[i]){
                    ret+=arr[j];
                }
            }
        }
        return  ret;
    }

使用归并排序改写

// 既要排好序  又要求小和
public static int smallSum(int[] arr){
   if (arr == null || arr.length < 2) {
       return 0;
   }
   return process(arr,0,arr.length-1);
}

public static int process(int[] arr,int L,int R){
   if (L == R){
       return 0;
   }
   int mid = L + ((R-L) >>1);
   // 返回左边合并之前的小和 + 右边合并之前的小和 + 两边合并之后的小和
   return process(arr,L,mid) + process(arr,mid + 1,R) + smallMerge(arr,L,mid,R);
}

public static int smallMerge(int[] arr,int L,int M,int R){
   int[] tempArr = new int[R - L + 1];
   int index = 0;
   int p1 = L;
   int p2 = M + 1;
   int ret = 0;

   while(p1 <= M && p2 <= R){
   	   // 相等的时候 右组先拷贝 并且不产生小和 
       ret += arr[p1] < arr[p2] ? (R - p2 +1) * arr[p1] : 0;
       // 谁小拷贝谁
       tempArr[index++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
   }

   while (p1 <= M){
       tempArr[index++] = arr[p1++];
   }
   while (p2 <= R){
       tempArr[index++] = arr[p2++];
   }
   for(int i = 0;i < tempArr.length;++i){
       arr[L + i]  = tempArr[i];
   }
   return  ret;
}

9 逆序对问题

逆序对问题 在一个数组中,左边的数如果比右边的数大,则这两个数
构成一个逆序对,请打印所有逆序对,求出逆序对的个数

    public static class InversePair {
        private int maxVal;
        private int minVal;

        public InversePair () {}

        public InversePair(int maxVal, int minVal) {
            this.maxVal = maxVal;
            this.minVal = minVal;
        }

        public int getMaxVal() {
            return maxVal;
        }

        public void setMaxVal(int maxVal) {
            this.maxVal = maxVal;
        }

        public int getMinVal() {
            return minVal;
        }

        public void setMinVal(int minVal) {
            this.minVal = minVal;
        }

        @Override
        public String toString() {
            return "InversePair{" +
                    "maxVal=" + maxVal +
                    ", minVal=" + minVal +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            InversePair that = (InversePair) o;
            return maxVal == that.maxVal &&
                    minVal == that.minVal;
        }

        @Override
        public int hashCode() {
            return Objects.hash(maxVal, minVal);
        }
    }

    private static List<InversePair> inversePairs = new ArrayList<>();

    public static void main(String[] args) {
        int[] arr = {8,7,6,5,4,3,2,1};
        System.out.println(getReverseOrderCnt(arr));
        for (InversePair pair : inversePairs) {
            System.out.println(pair);
        }
    }

    public static int getReverseOrderCnt(int[] arr){
        if (arr == null || arr.length < 2){
            return 0;
        }
        return process(arr,0,arr.length - 1);
    }

    public static int process(int[] arr,int L,int R) {
        if (L == R) {
            return 0;
        }
        int M = L + ((R -L) >> 1);
        return process(arr,L,M) + process(arr,M+1,R) + merge(arr,L,M,R);
    }

    public static int merge(int[] arr,int L,int M,int R){
        int[] tempArr = new int[R - L + 1];
        int p1 = L;
        int p2 = M + 1;
        int index = 0;
        int ret = 0;
        // 这里一定是有一边是拷贝完成的
        while (p1 <= M && p2 <= R){
            // 升序排列  谁大拷贝谁
            ret += arr[p1] > arr[p2] ? (R - p2 + 1) : 0;
            if (arr[p1] > arr[p2]){
                int tempIndex = p2;
                while (tempIndex <= R){
                   inversePairs.add(new InversePair(arr[p1],arr[tempIndex++]));
                }
            }
            tempArr[index++] = arr[p1] > arr[p2] ? arr[p1++] : arr[p2++];
        }

        while (p1 <= M) {
            tempArr[index++] = arr[p1++];
        }

        while (p2 <= R){
            tempArr[index ++] = arr[p2++];
        }

        for (int i = 0; i < tempArr.length ; i++) {
            arr[L + i] = tempArr[i];
        }
        return ret;
    }

10 归并排序非递归方式

public class MergeSortNoRecursionTest {

    public static void main(String[] args) {
        int[] arr = {10,7,6,2,10,3,90,23,12};
        mergeSortNoRecursion(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void mergeSortNoRecursion(int arr[]){
        if (arr == null || arr.length < 2){
            return;
        }
        int N = arr.length;
        // 步长
        int mergeSize = 1;
        while (mergeSize < N){
            // 当前左组的第一个位置
            int L = 0;
            // 左组的位置不能越界
            while (L < N){
                // 中间位置
                int M = L + mergeSize - 1;
                // 不够退出
                if (M >= N){
                    break;
                }
                // 右组的位置如果够 会来到 M + mergeSize  越界右组的位置就是 N -1
                int R = Math.min(M + mergeSize,N - 1 );
                merge(arr,L,M,R);
                // 左组的位置取到上一次右组位置的下一个位置
                L = R + 1;
            }
            // 防止溢出
            if (mergeSize > N /2){
                break;
            }
            // 步长乘2
            mergeSize <<= 1;
        }
    }

    public static void merge(int[] arr,int L,int M,int R){
        int[] tempArr = new int[R - L + 1];

        int index = 0;
        int p1 = L;
        int p2 = M + 1;

        while (p1 <= M && p2 <= R){
            tempArr[index++] = arr[p1] <= arr[p2] ? arr[p1 ++] : arr[p2 ++];
        }

        while (p1 <= M){
            tempArr[index ++] = arr[p1 ++];
        }

        while (p2 <= R){
            tempArr[index ++] = arr[p2 ++];
        }

        for (int  i = 0 ; i < tempArr.length;i++){
            arr[L + i] = tempArr[i];
        }
    }
}

11 快速排序

首先来看如下问题
荷兰国旗问题
问题1 : 给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)

解决步骤 :
划分小于的区域 一开始在最左边
遍历数组,如果当前值不超过num,则和小于等于区域的下一个数作交换,小于等于区域往右扩,如果当前数大于num,则直接遍历下一个,直到遍历完成结束
小于等于区域一直在推着等于区域往右走

package com.chao.test;

import java.util.Arrays;

/**
 * @author Mrchao
 * @version 1.0.0
 * @date 2023-07-22
 */
public class NetherlandsFlagTest {
    public static void main(String[] args) {
        int[] arr = {9,5,10,4,4,6,7,7,8,10,7,10};
        partition(arr,0,arr.length - 1,7);
        System.out.println(Arrays.toString(arr));
    }

    public static void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    /**
     * 给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,
     * 等于num的数放在数组的中间,大于num的数放在数组的右边。
     * 要求额外空间复杂度O(1),时间复杂度O(N)
     * @param arr
     * @param l 数组的左边界
     * @param r 数组的右边界
     * @param p 比较的数
     */
    public static void partition(int[] arr,int l,int r,int p) {
        // 小于区域的左边界
        int less = l - 1;
        // 大于区域右边界
        int more = r + 1;
        while (l < more){
            // 当前数小于给定的数 当前数和小于区域的下一个做交换 l继续往后走
            if (arr[l] <= p){
                swap(arr,l++,++less);
            } else { // 当前数大于给定的数 和大于区域的前一个数做交换 l不动
                swap(arr,l,--more);
            }
        }
    }
}

问题 2
给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)

处理步骤 :
划分小于区域的右边界 划分大于区域的左边界
遍历数组

  1. arr[i] < num [i]和 小于区域的下一个作交换, 小于区域往右扩,i++
  2. arr[i] == num 继续遍历 i ++
  3. arr[i] > num arr[i]和大于区域的前一个作交换 ,大于区域左扩,i原地不动
package com.chao.test;

import java.util.Arrays;

/**
 * @author Mrchao
 * @version 1.0.0
 * @date 2023-07-22
 */
public class NetherlandsFlagTest {
    public static void main(String[] args) {
        int[] arr = {9,5,10,4,4,6,7,7,8,10,7,10};
        partition(arr,0,arr.length - 1,7);
        System.out.println(Arrays.toString(arr));
    }

    public static void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    /**
     * 给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,
     * 等于num的数放在数组的中间,大于num的数放在数组的右边。
     * 要求额外空间复杂度O(1),时间复杂度O(N)
     * @param arr
     * @param l 数组的左边界
     * @param r 数组的右边界
     * @param p 比较的数
     */
    public static void partition(int[] arr,int l,int r,int p) {
        // 小于区域的左边界
        int less = l - 1;
        // 大于区域右边界
        int more = r + 1;
        while (l < more){
            // 当前数小于给定的数 当前数和小于区域的下一个做交换 l继续往后走
            if (arr[l] < p){
                swap(arr,l++,++less);
            //当前数大于给定的数 和大于区域的前一个数做交换 l不动
            } else if (arr[l] > p){ 
                swap(arr,l,--more);
            } else { // 直接下一个
                l ++;
            }
        }
    }
}

快排第一个版本
选数组中的最后一个数作划分值,将小于等于此划分值的数都放在数组的左边,然后将划分值和数组小于区域的下一个值做交换,则此划分值左边都是小于等于此划分值的,此值右侧都是大于此划分值的,然后让左侧和右侧递归重复此过程,最终会让整个数组都有序

    public static void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b]  = temp;
    }

    public static void quickSort(int[] arr){
        if (arr == null || arr.length < 2){
            return;
        }
        process(arr,0,arr.length - 1);
    }

    public static void  process(int[] arr,int L,int R){
        if (L >= R) {
            return;
        }
        int lessIndex = partition(arr, L, R);
        process(arr,L,lessIndex);
        process(arr,lessIndex + 2,R);
    }

    public static int partition(int[] arr,int L,int R){
        int pivot = arr[R];
        int less = L - 1;
        int more = R;
        while (L < more){
        	// 当前值比最右边的值小于或等于 当前值和小于区域的下一个值做交换
            if (arr[L] <= pivot){
                swap(arr,++less,L++);
            } else {
                swap(arr,L,--more);
            }
        }
        swap(arr,less + 1,R);
        // 返回小于区域的右边界
        return less;
    }

快排第二个版本

就是荷兰国旗问题
一开始选数组整个范围上的最后一个值作为划分值,划分出小于这个划分值的区域,等于这个划分值的区域,大于这个划分值的区域,然后将这个划分值和大于区域的第一个值做交换,则在排序过程中,此划分值在数组中的位置就不会动了

不改进的排序 :
1)划分值越靠近两侧,复杂度越高;划分值越靠近中间,复杂度越低
2)可以轻而易举的举出最差的例子,所以不改进的快速排序时间复杂度为O(N^2)

快速排序第三版本
如果划分值打在了中间,则满足master公式 T(N) = 2 * T(N/2) + O(N)

在数组的范围上[L,R],随机选择一个数作为划分值,好的情况和差的情况是等概率事件,把所有情况累加,再求数学上的长期期望,最后得到时间复杂度是 O(N*logN)
快排的额外空间复杂度O(logN)

coding

 /**
  * i != j
  *
  * @param arr
  * @param i
  * @param j
  */
 public static void swap(int[] arr, int i, int j) {
     int temp = arr[i];
     arr[i] = arr[j];
     arr[j] = temp;
 }

 public static void quickSort(int[] arr) {
     if (arr == null || arr.length < 2) {
         return;
     }
     quickSort(arr,0,arr.length -1);
 }

 public static void quickSort(int[] arr, int L, int R) {
     if (L < R) {
         // 随机选一个数 和最右边的数做交换
         swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
         int[] p = partition(arr,L,R);
         // p[0] 等于区域的左边界
         quickSort(arr,L,p[0] - 1); // 小于区域右边界
         // p[1] 等于区域的右边界
         quickSort(arr,p[1] + 1,R);// 大于区域左边界
     }
 }
 /**
  *
  * @param arr
  * @param L 分区的左边界
  * @param R 分区的右边界
  * @return  划分值等于区域的左边界和右边界
  */
 public static int[] partition(int[] arr, int L, int R) {
     // 小于区域的左边界
     int less = L - 1;
     // 大于区域的右边界
     int more = R; 
     while (L < more) { // L往右边走  more 往左边走
         // arr[R] 当前的划分值
         // 当前数小于划分值,当前值和小于区域的下一个值做交换
         if (arr[L] < arr[R]) { 
             swap(arr, ++less, L++);
          //当前数大于划分值 和大于区域的前一个值做交换   
         } else if (arr[L] > arr[R]) {
             swap(arr, --more, L);
         } else { // 等于的时候 直接往下走
             L++;
         }
     }
     // 大于区域的第一个数和最后一个数做交换
     swap(arr, more, R);
     // [等于区域的左边界,等于区域的右边界]
     return new int[]{less + 1, more};
 }
import java.util.Arrays;
import java.util.Stack;

/**
 * @author Mrchao
 * @version 1.0.0
 * @date 2023-07-23
 */
public class QuickSortNonRecurTest {
    public static void main(String[] args) {
        int[] arr = {5,2,6,7,10,21};
        quickSortNonRecur(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void  quickSortNonRecur(int[] arr){
        if (arr == null || arr.length < 2){
            return;
        }
        process(arr,0,arr.length - 1);
    }

    public static void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    public static void process(int[] arr,int leftBorder,int rightBorder){
        Stack<Integer> indexStack = new Stack<>();
        indexStack.push(rightBorder);
        indexStack.push(leftBorder);
        while (!indexStack.isEmpty()){
            int leftIndex = indexStack.pop();
            int rightIndex = indexStack.pop();
            int selectIndex = leftIndex + (int) Math.random() * (rightIndex - leftIndex + 1);
            swap(arr,selectIndex,rightBorder);
            int[] par = partition(arr, leftIndex, rightIndex);
            // 大于区域进栈
            if (par[1] + 1 < rightIndex){
                indexStack.push(rightIndex);
                indexStack.push(par[1] + 1);
            }
            // 小于区域进栈
            if (par[0] - 1 > leftIndex){
                indexStack.push(par[0] - 1);
                indexStack.push(leftIndex);
            }
        }

    }

    public static int[] partition(int[] arr,int leftIndex,int rightIndex){
        int less = leftIndex - 1;
        int more = rightIndex;
        while (leftIndex < more){
            if (arr[leftIndex] < arr[rightIndex]){
                swap(arr,++less,leftIndex++);
            }else if (arr[leftIndex] > arr[rightIndex]){
                swap(arr,--more,leftIndex);
            }else {
                ++ leftIndex;
            }
        }
        swap(arr,rightIndex,more);
        return new int[] {less + 1,more};
    }
}

12 堆排序

1 堆结构就是用数组实现的完全二叉树结构
2 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
3 完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
4 堆结构的 heapInsertheapify操作
5 堆结构的增大和减少
6 优先级队列结构,就是堆结构

什么是完全二叉树

若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树

完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树

完全二叉树的特点

  1. 叶子结点只可能在最大的两层上出现,对任意结点,若其右分支下的子孙最大层次为L,则其左分支下的子孙的最大层次必为L 或 L+1;
  2. 出于简便起见,完全二叉树通常采用数组而不是链表存储。
  3. 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
  4. 完全二叉树第i层至多有2*(i-1)个节点,共i层的完全二叉树最多有2*i-1个节点。
  5. 只允许最后一层有空缺结点且空缺在右边,即叶子结点只能在层次最大的两层上出现;
  6. 对任一结点,如果其右子树的深度为j,则其左子树的深度必为jj+1。 即度为1的点只有1个或0个

判断完全二叉树
完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树
数据结构与算法-排序算法_第2张图片

将数据转换成一个完全二叉树

在这里插入图片描述

将数组的索引作为二叉树的节点值
在这里插入图片描述

根节点索引和子树节点索引满足以下关系
设节点的索引为 i
其左子树节点的索引: 2 * i + 1
其右子树节点的索引 : 2 * i + 2
其父节点的索引为 : (i - 1 ) / 2

关于堆的两个操作
1 给定数组中某个位置的数,将这个数插入到已构建好的大根堆中 使大根堆依然是大跟堆

/**
 * 将数组中给定位置的数按大根堆的方式插入到堆中
 * @param arr
 * @param index
 */
public static void  heapInsert(int[] arr,int index){
    // 当前数比父节点的数大 就一直与父节点进行替换
    while (arr[index] > arr[(index - 1) /2 ]){
         swap(arr,index,(index - 1) / 2);
         index = index / 2;
     }
}

上面的操作可以理解从堆底一直往上蹿 直到遇到不小于它的数 时间复杂度是 O(logN)

2 将堆顶的数完成调整,将堆调整成大根堆

/**
* 从指定的位置构建大根堆
* @param arr
* @param index 指定的位置
* @param heapSize 堆中数的个数 空间数据是否属于堆
*/
public static void heapify(int[] arr,int index,int heapSize){
	// 左孩子的索引
	int left = (2 * index) + 1;
	// 存在左子节点
	while (left < heapSize){
	    // 两个子节点中  谁的值大就把索引给谁
	    int largest = (left + 1 < heapSize && arr[left + 1] > arr[left]) 
	    ? left + 1 : left;
	    // 父节点和子节点谁的值大 把值给 largest
	    largest = arr[largest] > arr[index] ? largest : index;
	    // 父节点的值比子节点的值大 不用交换 直接退出
	    if (largest == index){
	        break;
	    }
	    //较大的子节点和父节点进行交换
	    swap(arr,largest,index);
	    index = largest;
	    left = 2 * index + 1;
	}
}

上述操作的时间复杂度也是 O(logN)

堆排序

1 先让整个数组都变成大根堆结构,建立堆的过程:
	1)从上到下的方法,时间复杂度为O(N*logN)
	2)从下到上的方法,时间复杂度为O(N)
	
2	把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,
一直周而复始,时间复杂度为O(N*logN)

3	堆的大小减小成0之后,排序完成

堆排序的完整代码

import java.util.Arrays;

/**
 * @author Mrchao
 * @version 1.0.0
 * @date 2023-07-23
 */
public class HeapSortTest {
    public static void main(String[] args) {
        int[] arr = {10,3,7,4,6,2,8,90};
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    /**
     * 将数组中给定位置的数按大根堆的方式插入到堆中
     * @param arr
     * @param index
     */
    public static void  heapInsert(int[] arr,int index){
        // 当前数比父节点的数大 就一直与父节点进行替换
        while (arr[index] > arr[(index - 1) /2 ]){
             swap(arr,index,(index - 1) / 2);
             index = index / 2;
         }
    }

    /**
     * 从指定的位置构建大根堆
     * @param arr
     * @param index 指定的位置
     * @param heapSize 堆中数的个数 空间数据是否属于堆
     */
    public static void heapify(int[] arr,int index,int heapSize){
        // 左孩子的索引
        int left = (2 * index) + 1;
        // 存在左子节点
        while (left < heapSize){
            // 两个子节点中  谁的值大就把索引给谁
            int largest = (left + 1 < heapSize && arr[left + 1] > arr[left]) 
            ? left + 1 : left;
            // 父节点和子节点谁的值大 把值给 largest
            largest = arr[largest] > arr[index] ? largest : index;
            // 父节点的值比子节点的值大 不用交换 直接退出
            if (largest == index){
                break;
            }
            //较大的子节点和父节点进行交换
            swap(arr,largest,index);
            index = largest;
            left = 2 * index + 1;
        }
    }

    public static void heapSort(int[] arr){
        if (arr == null || arr.length < 2){
            return;
        }
        // 将数组中的所有元素加入大根堆中
        /*for (int  i = 0;i < arr.length;++i){
            heapInsert(arr,i); // O(logN)
        }*/
        for(int i = arr.length - 1;i >= 0; i--){
            heapify(arr,i,arr.length);
        }
        int heapSize = arr.length;
        swap(arr,0,--heapSize);
        while (heapSize > 0){// O(N)
            heapify(arr,0,heapSize);// O(logN)
            swap(arr,0,--heapSize);//O(1)
        }
    }
}

堆排序的时间复杂度 O(N * logN) 额外空间复杂度 O(1)

堆排序的相关算法

已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不
超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
/**
 *  已知一个几乎有序的数组 几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以
 * 不超过k并且k相对数组来说比较小  请选择一个核实排序算法对这个数据进行排序
 * @param arr
 * @param k
 */
public static void  sortedArrDistanceLessK(int[] arr,int k){
    // 默认小根堆
    PriorityQueue<Integer> heap = new PriorityQueue<>();
    // 构建小根堆
    int index = 0;
    // 先把前k个数放在堆中
    for (;index <= Math.min(arr.length,k);index++){
        heap.add(arr[index]);
    }

    int i = 0;
    // 从堆中放入一个 再取出一个
    for(;index < arr.length;i++,index++){
        heap.add(arr[index]);
        arr[i] = heap.poll();
    }

    while (!heap.isEmpty()){
        arr[i++] = heap.poll();
    }
}

13 桶排序

你可能感兴趣的:(数据结构与算法,排序算法,java,算法)