排序算法总结

https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

算法

时间复杂度

空间复杂度

稳定性

 

选择

O(N2)

O(1)

不稳定

 

冒泡

O(N2)

O(1)

稳定

 

插入

O(N2)

O(1)

稳定

 

归并

O(N*logN)

O(N)

稳定

 

堆排序

O(N*logN)

O(1)

不稳定

 

快速排序

O(N*logN)

O(logN)

不稳定

 

桶排序

O(N)

O(N)

稳定

 
     

 

辅助记忆

时间复杂度记忆-

冒泡、选择、直接 排序需要两个for循环,每次只关注一个元素,平均时间复杂度为O(n2)O(n2)(一遍找元素O(n),一遍找位置O(n))

快速、归并、希尔、堆基于二分思想,log以2为底,平均时间复杂度为O(nlogn)(一遍找元素O(n),一遍找位置O(logn)

稳定性记忆-“快希选堆”(快牺牲稳定性)

排序算法的稳定性:排序前后相同元素的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定

 

快速排序(重点)

基本思路:快速排序每一次都排定一个元素(这个元素呆在了它最终应该呆的位置),然后递归地去排它左边的部分和右边的部分,依次进行下去,直到数组有序;

算法思想:分而治之(分治思想),与「归并排序」不同,「快速排序」在「分」这件事情上不想「归并排序」无脑地一分为二,而是采用了 partition 的方法(书上,和网上都有介绍,就不展开了),因此就没有「合」的过程。

实现细节(注意事项):(针对特殊测试用例:顺序数组或者逆序数组)一定要随机化选择切分元素(pivot),否则在输入数组是有序数组或者是逆序数组的时候,快速排序会变得非常慢(等同于冒泡排序或者「选择排序」);

//双指针对撞partition

public int[] sortArray(int[] nums) {
        int len=nums.length;
        if(len==0){
            return new int[0];
        }
        quickSort(nums,0,len-1);
        return nums;

    }
    public void  quickSort(int[] nums,int left,int right){
        if (left < right) {
            //随机化选择切分元素(pivot),否则在输入数组是有序数组或者是逆序数组的
            //时候,快速排序会变得非常慢(等同于冒泡排序或者「选择排序」)
         swap(nums, left + (int) (Math.random() * (right - left + 1)), left);
            int index = partition(nums,left, right);//随机切分点
            quickSort(nums, left, index - 1); // 排序< 区
            quickSort(nums, index + 1, right); // 排序> 区
        }
    }
    public int partition(int[]nums,int left,int right){
        int pivot=nums[left];
        int i=left+1;
        int j=right;
        while(true){
            while(i<=j&&nums[i]<pivot){
                i++;
            }
            while(i<=j&&nums[j]>pivot){
                j--;
            }
            if(i>j){
                break;
            }
            swap(nums,i,j);
            i++;
            j--;
        }
        swap(nums,left,j);
        return j;
    }
    public void swap(int[] nums,int index1,int index2){
        int temp=nums[index1];
        nums[index1]=nums[index2];
        nums[index2]=temp;
    }

复杂度分析:

时间复杂度:O(NlogN),这里 N 是数组的长度;

空间复杂度:O(logN),这里占用的空间主要来自递归函数的栈空间

tips:快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后续遍历

快速排序的逻辑是,若要对nums[lo..hi]进行排序,我们先找一个分界点p,通过交换元素使得nums[lo..p-1]都小于等于nums[p],且nums[p+1..hi]都大于nums[p],然后递归地去nums[lo..p-1]和nums[p+1..hi]中寻找新的分界点,最后整个数组就被排序了。

快速排序的代码框架如下:

void sort(int[] nums, int lo, int hi) {

    /****** 前序遍历位置 ******/

    // 通过交换元素构建分界点 p

    int p = partition(nums, lo, hi);

    /************************/



    sort(nums, lo, p - 1);

    sort(nums, p + 1, hi);

}

先构造分界点,然后去左右子数组构造分界点,你看这不就是一个二叉树的前序遍历吗?

 

堆排序(重点)

堆排序是选择排序的优化,选择排序需要在未排定的部分里通过「打擂台」的方式选出最大的元素(复杂度 O(N)),而「堆排序」就把未排定的部分构建成一个「堆」,这样就能以O(logN) 的方式选出最大元素;

堆是一种相当有意思的数据结构,它在很多语言里也被命名为「优先队列」。它是建立在数组上的「树」结构,类似的数据结构还有「并查集」「线段树」等。「优先队列」是一种特殊的队列,按照优先级顺序出队,从这一点上说,与「普通队列」无差别。「优先队列」可以用数组实现,也可以用有序数组实现,但只要是线性结构,复杂度就会高,因此,「树」结构就有优势,「优先队列」的最好实现就是「堆」

堆排序 原理:

* 先将无序数组构成一个二叉堆(完全二叉树),使得每个节点都小于其子节点(兄弟节点之间可以无序),

* 每次取出根节点后,重新调整堆使其仍满足上述特性,每次取出的根节点就构成了一个有序数组。

* 建堆

* 用数组来存放整个二叉堆,且数组的首元素中存储着整个二叉堆的节点总个数,

* 建堆时总是在二叉堆的叶子节点处插入新节点,然后“上浮”该节点,最终在数组中得到一个二叉堆。

* 调整堆

* 每次在叶子节点处插入新节点即在数组末位插入新元素需要调整堆,比较新节点和其父节点的大小,

* 通过不断交换使其“上浮”,并最终位于二叉树的合适位置;

* 每次取出二叉堆的根节点即取出数组的第二个元素后需要调整堆,将二叉树的最后一个叶子节点作为新的根节点,

* 然后依次比较其和子节点的大小,通过不断交换使其“下沉”,并最终位于二叉树的合适位置

public static int[] sortArray(int[] nums) {
    int len = nums.length;
    // 将给定的数组整理成堆
    heapify(nums);
    
    //数组第一次整理成堆以后,堆顶就是数组的最大值
    // 循环不变量:区间 [0, i] 堆有序
    for (int i = len - 1; i >= 1; ) {
        // 把堆顶元素(当前最大)交换到数组末尾
        swap(nums, 0, i);
        // 逐步减少堆有序的部分
        i--;
        // 下标 0 位置下沉操作,使得区间 [0, i] 堆有序,将剩下的数组在整理成大根堆,继续将堆顶元素交换到数组末尾i上
        siftDown(nums, 0, i);
    }
    return nums;
}

/**
 * 将数组整理成堆(堆有序)
 *
 * @param nums
 */
private  static void heapify(int[] nums) {//给定一个数组,第一次将整个数组调整成大根堆
    int len = nums.length;
    // 只需要从 i = (len - 1) / 2 这个位置开始逐层下移
    for (int i = (len - 1) / 2; i >= 0; i--) {
        siftDown(nums, i, len - 1);
    }
}

/**
 * @param nums
 * @param k    当前下沉元素的下标
 * @param end  [0, end] 是 nums 的有效部分
 */
private static void siftDown(int[] nums, int k, int end) {
    while (2 * k + 1 <= end) {//如果2 * k + 1 > end,则k为叶子节点,没有子节点,超过了整棵树。或者其子节点已经排序完成,不需要再调整
        int j = 2 * k + 1;//k的左子树
        if (j + 1 <= end && nums[j + 1] > nums[j]) {//右子树>左子树
            j++;
        }
        if (nums[j] > nums[k]) {//现在的j是左子树和右子树种较大的那个的索引
            swap(nums, j, k);//如果左子树或者右子树比父节点的值 大,就将较大的值换到父节点上
        } else {//否则则是已经有序
            break;
        }
        k = j;//原来的父节点已经下沉到比他大的子节点上去了,在接着判断这个下沉的父节点还有没有比他更大的子节点
    }
}

private static void swap(int[] nums, int index1, int index2) {
    int temp = nums[index1];
    nums[index1] = nums[index2];
    nums[index2] = temp;
}

复杂度分析:

  • 时间复杂度:O(NlogN),这里 N 是数组的长度;
  • 空间复杂度:O(1)

归并排序(重点)

基本思路:借助额外空间,合并两个有序数组,得到更长的有序数组。

* 将数组均分成两个子数组,先将两个子数组分别进行排序,然后合并得到全体元素都有序的数组,

* 为使上述的两个子数组分别有序,需要先对其各自的两个子数组进行排序再合并,因此需要递归地对每个子数组的两个子数组进行归并排序,直到子数组只有2个元素,此时只需要直接进行合并

分: 不断将数组从中点位置划分开(即二分法),将整个数组的排序问题转化为子数组的排序问题;

治: 划分到子数组长度为 1 时,开始向上合并,不断将 较短排序数组 合并为 较长排序数组,直至合并至原数组时完成排序

public static void mergeSort(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) {//basecase,
      return;
   }
   int mid = L + ((R - L) >> 1);
   process(arr, L, mid);//最后就二分成两个数组分别只有一个元素
   process(arr, mid + 1, R);//执行完上一个的basecase才会执行这条语句
   merge(arr, L, mid, R);
}

public static void merge1(int[] arr, int left, int mid, int right) {
   int[] help = new int[right - left + 1];
   int i = 0;//辅助数组索引
   int p1 = left;//有序数组1的索引指针
   int p2 = mid + 1;//有序数组2的索引指针
   while (p1 <= mid && p2 <= right) {//将两组有序的数组[left mid][mid+1,right]进行归并
      help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];//谁小就先把谁放入辅助数组。指针++
   }
   while (p1 <= mid) {//当第二个数组已经遍历完 ,直接将有序数组1放入
      help[i++] = arr[p1++];
   }
   while (p2 <= right) {//当第1个数组已经遍历完 ,直接将有序数组2放入
      help[i++] = arr[p2++];
   }
   for (i = 0; i < help.length; i++) {//将辅助数组在遍历放回nums数组
      arr[left + i] = help[i];
   }
}

归并的两种写法,用while和if

private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {
        System.arraycopy(nums, left, temp, left, right + 1 - left);
        //复制一个一维空数组,防止修改原数组,源数组是nums,复制的起始位置left,目标数组temp,
        //目标数组的开始起始位置left,复制的数组长度为right + 1 - left

        int i = left;
        int j = mid + 1;

        for (int k = left; k <= right; k++) {
            if (i == mid + 1) {//数组1已经归并完成
                nums[k] = temp[j];
                j++;
            } else if (j == right + 1) {//数组2已经归并完成
                nums[k] = temp[i];
                i++;
            } else if (temp[i] <= temp[j]) {
                // 注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
                nums[k] = temp[i];
                i++;
            } else {
                // temp[i] > temp[j]
                nums[k] = temp[j];
                j++;
            }
}

「归并排序」比「快速排序」好的一点是,它借助了额外空间,可以实现「稳定排序」。

复杂度分析:

时间复杂度:O(NlogN),这里 N 是数组的长度;

空间复杂度:O(N),辅助数组与输入数组规模相当。

tips:再说说归并排序的逻辑,若要对nums[lo..hi]进行排序,我们先对nums[lo..mid]排序,再对nums[mid+1..hi]排序,最后把这两个有序的子数组合并,整个数组就排好序了。

归并排序的代码框架如下:

void sort(int[] nums, int lo, int hi) {

    int mid = (lo + hi) / 2;

    sort(nums, lo, mid);

    sort(nums, mid + 1, hi);



    /****** 后序遍历位置 ******/

    // 合并两个排好序的子数组

    merge(nums, lo, mid, hi);

    /************************/

}

先对左右子数组排序,然后合并(类似合并有序链表的逻辑),你看这是不是二叉树的后序遍历框架?另外,这不就是传说中的分治算法嘛,不过如此呀

 

 

插入排序(熟悉)

// 插入排序:稳定排序,在接近有序的情况下,表现优异

原理: * 确保数组前i-1个元素已经排好序,在第i轮循环时,将第i个元素从后往前依次和 * 其前面的元素比较和交换,最后插入到前i-1个有序的子数组中的合适位置

public static void insertionSort(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   //方法1
   for (int i = 1; i < arr.length; i++) {//从第二个元素开始调整,直到最后一个元素
      for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {//对i之前的所有元素进行遍历判断
         swap(arr, j, j + 1);
      }
      //对方法1进行优化
      //插入排序改进
      // 如果当前元素已经位于正确的位置,则不必继续往前插入,可以提前结束本轮循环
       for (int i = 1; i < a.length; ++i) {
        for (int j = i; j > 0; --j) {
            if (a[j - 1] > a[j])
                swap(a, j - 1, j);
            else
                break;
        }
   }
}

public static int[] sortArray(int[] nums) {
   int len = nums.length;
   // 循环不变量:将 nums[i] 插入到区间 [0, i) 使之成为有序数组
   for (int i = 1; i < len; i++) {//从第二个元素开始调整,直到最后一个元素
      // 先暂存这个元素,然后之前元素逐个后移,留出空位
      int temp = nums[i];
      int j = i;
      // 注意边界 j > 0
      while (j > 0 && nums[j - 1] > temp) {
         nums[j] = nums[j - 1];//将前面大于temp该值的元素依次后移
         j--;//继续往前判断
      }
      nums[j] = temp;//当前面的元素都不在大于temp,不再满足上面的while,这时再将temp放入该放入的位置
   }
   return nums;
}

复杂度分析:

  • 时间复杂度:O(N2),这里 N 是数组的长度;
  • 空间复杂度:O(1),使用到常数个临时变量

选择排序(了解)

原理: * 从数组的首元素开始,使用i指针指向索引位置,标记为最小值,用另一个指针在剩下的元素里面遍历,如果遍历当前元素比这个最小元素大,则交换之,使得第i个位置的元素值为第i小,相当于每一轮循环是在所有未排序的元素之中选择出最小的元素

每一轮选取未排定的部分中最小的部分交换到未排定部分的最开头,经过若干个步骤,就能排定整个数组。即:先选出最小的,再选出第 2 小的,以此类推

public static void selectionSort(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   for (int i = 0; i < arr.length - 1; i++) {//i位置找第i小的元素,最后一个位置不用找
      int minIndex = i;
      for (int j = i + 1; j < arr.length; j++) {
         minIndex = arr[j] < arr[minIndex] ? j : minIndex;
      }
      swap(arr, i, minIndex);
   }
}

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

复杂度分析:

  • 时间复杂度:O(N2),这里 N 是数组的长度;
  • 空间复杂度:O(1),使用到常数个临时变量。

冒泡排序(了解)

原理: * 每一轮循环中依次比较相邻位置上元素的大小,使得较大的元素后移, * 且确保第i轮循环完之后能把第i大的元素移动到排序后的位置上

改进: * 每一轮循环开始前设置标志位,如果本轮循环没有交换任何元素, * 则说明所有元素已经有序,可以提前结束排序

public int[] bubbleSort(int[] nums) {
        int len = nums.length;
        for (int i = len - 1; i >= 0; i--) {
            // 先默认数组是有序的,只要发生一次交换,就必须进行下一轮比较,
            // 如果在内层循环中,都没有执行一次交换操作,说明此时数组已经是升序数组
            boolean sorted = true;//每一轮冒泡之前重置标志位
            for (int j = 0; j < i; j++) {
                if (nums[j] > nums[j + 1]) {
                    swap(nums, j, j + 1);//用j指针做内层遍历,将数组arr[j,i]中的最大值移动到i位置上,
                    sorted = false;
                }
            }
            if (sorted) {
                break;//已经全部排好序
            }
        }
        return nums;
    }

    private void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
}

计数排序(了解)

基数排序(了解)

桶排序(了解)

希尔排序(不要求)

https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值