重生之数据结构与算法—-常见排序算法(二)

简介

上文中,我们讲到了选择排序,冒泡排序,插入排序,希尔排序。
都是相对比较简单的实现方式,因为它们都是以人的思维,维护一个index,将index与周围元素逐步对比。直到整个数组有序。
但越是效率高的算法,反而要越接近计算的的思维。否则非常难以突破O(N^2)的桎梏。
而接下来的几种效率高算法,则是一步一步接近计算机的思维,实现排序高效。

二叉树前序遍历:快速排序

快速排序的核心思路是:先将一个元素排好序,然后递归排序剩下的元素

这里可能难以理解,这说的是什么逼话?我们再拆解一下。

  1. 在数组中随机选一个作为排序元素
[3,1,4,1,5,9,2,6]  ^ pivot 
  1. 进行排序,将大于pivot的元素放右边,小于的放左边
[1,1,2,3,4,5,9,6]        ^ 	 pivot 
  1. 递归重复以上步骤,寻找新的切分元素,然后交换。
[[1,1,2],3,  [4,5,9,6]]   ^      ^    ^ pivot2 pivot pivot3 
  1. 直到全部排序完成
[1,1,2,3,4,5,6,9] 

根据上面的思维描述,我们可以写出一个代码框架,并将它们抽象成一颗二叉树

        public void Sort(int[] arr,int left,int right)         {             if (left > right)                 return;              //进行切分,并将P排好序             int p = Partition(arr, left, right);              //对左右子数             //是不是类似二叉树的前序遍历?             Sort(arr, left, p - 1);             Sort(arr, p + 1, right);         } 		 		        /// <summary>         /// 分区操作         /// </summary>         /// <param name="arr"></param>         /// <param name="left"></param>         /// <param name="right"></param>         /// <returns></returns>         private static int Partition(int[] arr, int left, int right)         {             // 选择最右边的元素作为基准元素             int pivot = arr[right];             int i = left - 1;              for (int j = left; j < right; j++)             {                 if (arr[j] <= pivot)                 {                     i++;                     (arr[i], arr[j]) = (arr[j], arr[i]);                 }             }             (arr[i + 1], arr[right]) = (arr[right], arr[i + 1]);             return i + 1;         } 

重生之数据结构与算法----常见排序算法(二)

复杂度分析

  1. 时间复杂度
    在理想情况下,这是一颗平衡二叉树,但在极端情况下退化成单链表。因此时间复杂度平均为O(n log n),最坏为O(n ^2)
  2. 空间复杂度
    平均情况下为O(log n),最坏情况下为O(n),主要取决于递归调用栈的深度
  3. 排序稳定性
    排序不稳定,因此排序稳定的前提是交换左右元素,而Partition会带来切分,所以这是可以推理出来的。
  4. 原地排序
    快速排序不需要额外的辅助空间,所以是原地排序算法。在遍历二叉树时,递归深度为树的高度,所以空间复杂度为O(log n)

二叉树的后序遍历:归并排序

上面说到,快速排序的核心思路是先将一个元素排好序,然后递归排序剩下的元素。
而归并排序的思路是,把数组切成两半,然后单独排序,最后再合并。

        public void Sort(int[] arr)         {             if (arr.Length <= 1)                 return;             int mid = arr.Length / 2;              //归并排序需要空间来合并有序数组,复杂度O(N)             int[] left = new int[mid];             int[] right = new int[arr.Length - mid];             Array.Copy(arr, 0, left, 0, mid);             Array.Copy(arr, mid, right, 0, arr.Length - mid);              Sort(left);             Sort(right);              //合并有序数组,是不是有点类似二叉树的后序位置?             Merge(arr, left, right);         }          private static void Merge(int[] arr, int[] left, int[] right)         {             int i = 0, j = 0, k = 0;             while (i < left.Length && j < right.Length)             {                 if (left[i] < right[j])                 {                     arr[k] = left[i];                     i++;                 }                 else                 {                     arr[k] = right[j];                     j++;                 }                 k++;             }             while (i < left.Length)             {                 arr[k] = left[i];                 i++;                 k++;             }             while (j < right.Length)             {                 arr[k] = right[j];                 j++;                 k++;             }         } 

复杂度分析

  1. 时间复杂度
    归并排序的时间复杂度始终为O(n log n),其中 n 是数组的长度。这是因为每次分解数组的时间复杂度为O(log n),而每次合并数组的时间复杂度为O(n)。
  2. 空间复杂度
    O(n),主要用于存储合并过程中临时创建的数组。
  3. 排序稳定性
    稳定排序,因为在合并过程中,当两个元素相等时,会优先选择左边子数组的元素,从而保证相同元素的相对顺序不变。
  4. 原地排序
    不是原地排序,因为需要额外的辅助数组。

二叉堆的妙用:堆排序

堆排序是二叉堆衍生出来的排序算法,核心分为两步。第一在原数组上建堆(大顶堆 or 小顶堆),在进行原地排序。

最简单的堆排序算法,就是直接利用二叉堆的优先级队列,然后用一个数组存储结果不就好了吗?

        public static void Run()         {             //创建一个小顶堆             var q = new PriorityQueueSimple(10);             q.Push(3);             q.Push(2);             q.Push(1);             q.Push(5);             q.Push(4);              var arr = new int[q.Count];             for (int i = 0; i < arr.Length; i++)             {                 arr[i] = q.Pop();                  Console.WriteLine(arr[i]);             }         } 

这里带来的问题是,我们额外用了一个数组来存储元素。而我们希望是原地排序,不带来额外的空间复杂度。

因此,我们优化后的代码就变成了:

    public class HeapSort     {         public static void Run()         {             var t =new HeapSort();             //第一步,原地建堆             var arr = new int[] { 3, 1, 4, 1, 5, 9, 2, 6 };             for (int i = 0; i < arr.Length; i++)             {                 t.MinHeapSwim(arr, i);             }              //第二部,排序             var length = arr.Length;             while (length > 0)             {                 //删除堆顶,并放到堆最后面                 (arr[0], arr[length - 1]) = (arr[length - 1], arr[0]);                 length--;                  //下浮                 t.MinHeapSink(arr, 0, length);             }                           for (int i = arr.Length-1; i>=0; i--)             {                 Console.WriteLine(arr[i]);             }         }         /// <summary>         /// 小顶堆的上浮         /// </summary>         /// <param name="heap"></param>         /// <param name="node"></param>         void MinHeapSwim(int[] heap,int node)         {             while(heap[node] < heap[Parent(node)])             {                 //swap                 (heap[Parent(node)], heap[node]) = (heap[node], heap[Parent(node)]);                 node = Parent(node);             }         }                  void MinHeapSink(int[] heap, int node,int count)         {             while (Left(node) < count || Right(node) < count)             {                 int minNode = node;                 if (Left(node) < count&& heap[node] > heap[Left(node)])                 {                     minNode = Left(node);                 }                  if (Right(node) < count && heap[minNode] > heap[Right(node)])                 {                     minNode = Right(node);                 }                  if (minNode == node)                 {                     break;                 }                 //swap                 (heap[node], heap[minNode]) = (heap[minNode], heap[node]);             }                         }          // 父节点的索引         int Parent(int node)         {             return (node - 1) / 2;         }          // 左子节点的索引         int Left(int node)         {             return node * 2 + 1;         }          // 右子节点的索引         int Right(int node)         {             return node * 2 + 2;         }      } 

其实没什么变化,相对之前实现的优先级队列来说。只是把数组作为参数传递,实现原地排序而已。

复杂度分析

  1. 时间复杂度
    O(n log n) ,因为要对swim/sink要对每个元素调用
  2. 空间复杂度
    O(1)
  3. 排序稳定性
    不稳定,因为skin过程中,要将堆顶元素,与堆尾元素交换。 违背了相邻元素交换的原则,所以不稳定。
  4. 原地排序
    是,我们的优化过程就是为了结果原地排序的问题。
发表评论

评论已关闭。

相关文章