最新消息:

比较排序算法及复杂度分析

工作 admin 2192浏览 0评论

比较排序算法分类

比较排序(Comparison Sort)通过对数组中的元素进行比较来实现排序。

比较排序算法(Comparison Sorts)

Category

Name

Best

Average Worst Memory Stability
 插入排序 (Insertion Sorts)

 

 插入排序(Insertion Sort) n n2 n2 1 Stable
 希尔排序(Shell Sort) n n log2 n n log2 n 1 Not Stable
 交换排序(Exchange Sorts )

 

 

 

 快速排序(Quick Sort)  n log n
n log n n2 log n Not Stable
 冒泡排序(Bubble Sort) n n2 n2 1 Stable
 鸡尾酒排序(Cocktail Sort) n n2 n2 1 Stable
 奇偶排序(Odd-Even Sort) n n2 n2 1 Stable
 选择排序(Selection Sorts)

 

 选择排序(Selection Sort)  n2 n2 n2 1 Not Stable
 堆排序(Heap Sort)  n log n n log n n log n 1 Not Stable
 合并排序(Merge Sorts)  合并排序(Merge Sort)  n n log n n log n n Stable
 混合排序(Hybrid Sorts)  内省排序(Introspective Sort)  n log n n log n n log n log n Not Stable

注:关于 Memory,如果算法为 “in place” 排序,则仅需要 O(1) 内存;有时对于额外的 O(log(n)) 内存也可以称为 “in place”。

注:Microsoft .NET Framework 中 Array.Sort 方法的实现使用了内省排序(Introspective Sort)算法

Stable 与 Not Stable 的比较

稳定排序算法会将相等的元素值维持其相对次序。如果一个排序算法是稳定的,当有两个有相等的元素值 R 和 S,且在原本的列表中 R 出现在 S 之前,那么在排序过的列表中 R 也将会是在 S 之前。

jinglingshu_2014-07-21_14-40-21

O(n2) 与 O(n*logn) 的比较

jinglingshu_2014-07-21_14-40-22

合并排序和堆排序在最坏情况下达到上界 O(n*logn),快速排序在平均情况下达到上界 O(n*logn)。对于比较排序算法,我们都能给出 n 个输入的数值,使算法以 Ω(n*logn) 时间运行。

注:有关算法复杂度,可参考文章《算法复杂度分析》。有关常用数据结构的复杂度,可参考文章《常用数据结构及复杂度》。

冒泡排序(Bubble Sort)

算法描述

重复地比较要排序的数列,一次比较两个元素,如果后者较小则与前者交换元素。

  1. 比较相邻的元素,如果前者比后者大,则交换两个元素。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。
  3. 针对所有的元素重复以上的步骤,除了最后一个。

jinglingshu_2014-07-21_14-40-25

算法复杂度

冒泡排序对 n 个元素需要 O(n2) 的比较次数,且可以原地排序。冒泡排序仅适用于对于含有较少元素的数列进行排序。

  • 最差时间复杂度 O(n2)
  • 平均时间复杂度 O(n2)
  • 最优时间复杂度 O(n)
  • 最差空间复杂度 O(n),辅助空间 O(1)

示例代码

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 };

      OptimizedBubbleSort(unsorted);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }

    static void BubbleSort(int[] unsorted)
    {
      for (int i = 0; i < unsorted.Length; i++)
      {
        for (int j = 0; j < unsorted.Length - 1 - i; j++)
        {
          if (unsorted[j] > unsorted[j + 1])
          {
            int temp = unsorted[j];
            unsorted[j] = unsorted[j + 1];
            unsorted[j + 1] = temp;
          }
        }
      }
    }

    static void OptimizedBubbleSort(int[] unsorted)
    {
      int exchange = unsorted.Length - 1;
      while (exchange > 0)
      {
        int lastExchange = exchange;
        exchange = 0;

        for (int i = 0; i < lastExchange; i++)
        {
          if (unsorted[i] > unsorted[i + 1])
          {
            int temp = unsorted[i];
            unsorted[i] = unsorted[i + 1];
            unsorted[i + 1] = temp;

            exchange = i;
          }
        }
      }
    }
  }

鸡尾酒排序(Cocktail Sort)

算法描述

鸡尾酒排序,也就是双向冒泡排序(Bidirectional Bubble Sort),是冒泡排序的一种变形。此算法与冒泡排序的不同处在于排序时是以双向在序列中进行排序。如果序列中的大部分元素已经排序好时,可以得到比冒泡排序更好的性能。

jinglingshu_2014-07-21_14-40-261

算法复杂度

  • 最差时间复杂度 O(n2)
  • 平均时间复杂度 O(n2)
  • 最优时间复杂度 O(n)
  • 最差空间复杂度 О(1)

代码示例

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 };

      OptimizedCocktailSort(unsorted);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }

    static void CocktailSort(int[] unsorted)
    {
      for (int i = 0; i < unsorted.Length / 2; i++)
      {
        // move the larger to right side
        for (int j = i; j + 1 < unsorted.Length - i; j++)
        {
          if (unsorted[j] > unsorted[j + 1])
          {
            int temp = unsorted[j];
            unsorted[j] = unsorted[j + 1];
            unsorted[j + 1] = temp;
          }
        }

        // move the smaller to left side
        for (int j = unsorted.Length - i - 1; j > i; j--)
        {
          if (unsorted[j - 1] > unsorted[j])
          {
            int temp = unsorted[j - 1];
            unsorted[j - 1] = unsorted[j];
            unsorted[j] = temp;
          }
        }
      }
    }

    static void OptimizedCocktailSort(int[] unsorted)
    {
      bool swapped = false;
      int start = 0;
      int end = unsorted.Length - 1;
      do
      {
        swapped = false;

        // move the larger to right side
        for (int i = start; i < end; i++)
        {
          if (unsorted[i] > unsorted[i + 1])
          {
            int temp = unsorted[i];
            unsorted[i] = unsorted[i + 1];
            unsorted[i + 1] = temp;

            swapped = true;
          }
        }

        // we can exit the outer loop here if no swaps occurred.
        if (!swapped) break;
        swapped = false;
        end = end - 1;

        // move the smaller to left side
        for (int j = end; j > start; j--)
        {
          if (unsorted[j - 1] > unsorted[j])
          {
            int temp = unsorted[j];
            unsorted[j] = unsorted[j - 1];
            unsorted[j - 1] = temp;

            swapped = true;
          }
        }

        start = start + 1;
      }
      while (swapped);
    }
  }

奇偶排序(Odd-Even Sort)

奇偶排序通过比较数组中相邻的(奇-偶)位置元素,如果该奇偶元素对是错误的顺序(前者大于后者),则交换元素。然后再针对所有的(偶-奇)位置元素进行比较。如此交替进行下去

jinglingshu_2014-07-21_14-40-26

 

算法复杂度

  • 最差时间复杂度 O(n2)
  • 平均时间复杂度 O(n2)
  • 最优时间复杂度 O(n)
  • 最差空间复杂度 О(1)

代码示例

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 };

      OptimizedOddEvenSort(unsorted);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }

    static void OddEvenSort(int[] unsorted)
    {
      for (int i = 0; i < unsorted.Length; ++i)
      {
        if (i % 2 > 0)
        {
          for (int j = 2; j < unsorted.Length; j += 2)
          {
            if (unsorted[j] < unsorted[j - 1])
            {
              int temp = unsorted[j - 1];
              unsorted[j - 1] = unsorted[j];
              unsorted[j] = temp;
            }
          }
        }
        else
        {
          for (int j = 1; j < unsorted.Length; j += 2)
          {
            if (unsorted[j] < unsorted[j - 1])
            {
              int temp = unsorted[j - 1];
              unsorted[j - 1] = unsorted[j];
              unsorted[j] = temp;
            }
          }
        }
      }
    }

    static void OptimizedOddEvenSort(int[] unsorted)
    {
      bool swapped = true;
      int start = 0;

      while (swapped || start == 1)
      {
        swapped = false;

        for (int i = start; i < unsorted.Length - 1; i += 2)
        {
          if (unsorted[i] > unsorted[i + 1])
          {
            int temp = unsorted[i];
            unsorted[i] = unsorted[i + 1];
            unsorted[i + 1] = temp;

            swapped = true;
          }
        }

        if (start == 0) start = 1;
        else start = 0;
      }
    }
  }

快速排序(Quick Sort)

快速排序使用分治法(Divide-and-Conquer)策略将一个数列分成两个子数列并使用递归来处理。

算法描述

  1. 从数列中挑出一个元素,称为 “主元”(pivot)。
  2. 重新排序数列,所有元素比主元小的摆放在主元前面,所有元素比主元值大的摆在主元的后面(相同的数可以到任一边)。这个称为分区(partition)操作。在分区退出之后,该主元就处于数列的中间位置。
  3. 递归地(recursively)把小于主元值元素的子数列和大于主元值元素的子数列排序。

递归的最底部情形,是数列的大小是 0 或 1 ,也就是总是被排序好的状况。这样一直递归下去,直到算法退出。

jinglingshu_2014-07-21_14-40-30

算法复杂度

  • 最差时间复杂度 O(n2)
  • 平均时间复杂度 O(n*log n)
  • 最优时间复杂度 O(n*log n)
  • 最差空间复杂度 根据实现的方式不同而不同 O(n) 辅助空间 O(log n)

快速排序的运行时间与划分是否对称有关,而后者又与选择了哪一个元素来进行划分有关。如果划分是对称的,那么快速排序从渐进意义上来讲,就与合并算法一样快;如果划分是不对称的,那么从渐进意义上来讲,就与插入排序一样慢。

快速排序的平均运行时间与其最佳情况运行时间很接近,而不是非常接近于最差情况运行时间。

快速排序的随机化版本是对足够大的输入的理想选择。

算法比较

快速排序是二叉查找树的一个空间优化版本。但其不是循序地把数据项插入到一个显式的树中,而是由快速排序组织这些数据项到一个由递归调用所隐含的树中。这两个算法完全地产生相同的比较次数,但是顺序不同。

快速排序的最直接竞争者是堆排序(Heap Sort)。堆排序通常会慢于原地排序的快速排序,其最坏情况的运行时间总是 O(n log n) 。快速排序通常情况下会比较快,但仍然有最坏情况发生的机会。

快速排序也会与合并排序(Merge Sort)竞 争。合并排序的特点是最坏情况有着 O(n log n) 运行时间的优势。不像快速排序或堆排序,合并排序是一个稳定排序算法,并且非常的灵活,其设计可以应用于操作链表,或大型链式存储等,例如磁盘存储或网路 附加存储等。尽管快速排序也可以被重写使用在链表上,但对于基准的选择总是个问题。合并排序的主要缺点是在最佳情况下需要 O(n) 额外的空间,而快速排序的原地分区和尾部递归仅使用 O(log n) 的空间。

代码示例

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 };

      QuickSort(unsorted, 0, unsorted.Length - 1);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }

    // left is the index of the leftmost element of the subarray
    // right is the index of the rightmost element of the subarray (inclusive)
    // number of elements in subarray = right-left+1
    static void QuickSort(int[] unsorted, int left, int right)
    {
      if (left < right)
      {
        int pivotIndex = (left + right) / 2;
        int pivotValue = unsorted[pivotIndex];

        int i = left - 1;
        int j = right + 1;
        while (true)
        {
          while (unsorted[++i] < pivotValue) ;
          while (unsorted[--j] > pivotValue) ;
          if (i >= j) break;

          int temp = unsorted[i];
          unsorted[i] = unsorted[j];
          unsorted[j] = temp;
        }

        QuickSort(unsorted, left, i - 1);
        QuickSort(unsorted, j + 1, right);
      }
    }
  }

选择排序(Selection Sort)

算法原理

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

jinglingshu_2014-07-21_14-40-30

算法复杂度

选择排序的交换操作介于 0 和 (n-1) 次之间。选择排序的比较操作为 n(n-1)/2 次之间。选择排序的赋值操作介于 0 和 3(n-1) 次之间。

比较次数 O(n2),比较次数与关键字的初始状态无关,总的比较次数 N = (n-1)+(n-2)+…+1 = n*(n-1)/2。交换次数 O(n),最好情况是,已经有序,交换 0 次;最坏情况是,逆序,交换 n-1 次。交换次数比冒泡排序较少,由于交换所需 CPU 时间比比较所需的 CPU 时间多,n 值较小时,选择排序比冒泡排序快。

原地操作几乎是选择排序的唯一优点,当空间复杂度(space complexity)要求较高时,可以考虑选择排序,实际适用的场合非常罕见。

  • 最差时间复杂度 О(n²)
  • 平均时间复杂度 О(n²)
  • 最优时间复杂度 О(n²)
  • 最差空间复杂度 О(n),辅助空间 O(1)

代码示例

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 };

      SelectionSort(unsorted);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }

    static void SelectionSort(int[] unsorted)
    {
      // advance the position through the entire array
      // could do i < n-1 because single element is also min element
      for (int i = 0; i < unsorted.Length - 1; i++)
      {
        // find the min element in the unsorted a[i .. n-1]
        // assume the min is the first element
        int min = i;

        // test against elements after i to find the smallest
        for (int j = i + 1; j < unsorted.Length; j++)
        {
          // if this element is less, then it is the new minimum
          if (unsorted[j] < unsorted[min])
          {
            // found new minimum, remember its index
            min = j;
          }
        }

        // swap
        if (min != i)
        {
          int temp = unsorted[i];
          unsorted[i] = unsorted[min];
          unsorted[min] = temp;
        }
      }
    }
  }

插入排序(Insertion Sort)

算法原理

对于未排序数据,在已排序序列中从后向前扫描,找到相应位置,将位置后的已排序数据逐步向后挪位,将新元素插入到该位置。

jinglingshu_2014-07-21_14-40-32

算法描述

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤 2~5;

算法复杂度

  • In-Place 原地排序(即只需要用到 O(1) 的额外空间)
  • 最差时间复杂度 O(n2)
  • 平均时间复杂度 O(n2)
  • 最优时间复杂度 O(n)
  • 最差空间复杂度 O(n),辅助空间 O(1)

插入排序算法的内循环是紧密的,对小规模输入来说是一个快速的原地排序算法。

示例代码

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 };

      InsertionSort(unsorted);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }

    static void InsertionSort(int[] unsorted)
    {
      for (int i = 1; i < unsorted.Length; i++)
      {
        if (unsorted[i - 1] > unsorted[i])
        {
          int key = unsorted[i];
          int j = i;
          while (j > 0 && unsorted[j - 1] > key)
          {
            unsorted[j] = unsorted[j - 1];
            j--;
          }
          unsorted[j] = key;
        }
      }
    }
  }

希尔排序(Shell Sort)

希尔排序是插入排序的一种更高效的改进版本,其基于插入排序的以下两个特点提出改进方法:

  • 插入排序在对几乎已经排序的数据操作时,效率高,可以达到线性时间。
  • 插入排序一般来说是低效的,其每次只能将数据移动一位。

算法描述

希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了,此时插入排序较快。

假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为 O(n2) 的排序(冒泡排序或插入排序),可能会进行 n 次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少量比较和交换即可移到正确位置。

jinglingshu_2014-07-21_14-40-36

步长序列(Gap Sequences)

步长的选择是希尔排序的重要部分。只要最终步长为 1 任何步长串行都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为 1 进行排序。当步长为 1 时,算法变为插入排序,这就保证了数据一定会被排序。

已知的最好步长串行是由 Sedgewick 提出的 (1, 5, 19, 41, 109,…),该步长的项来自 9 * 4^i – 9 * 2^i + 1 和 4^i – 3 * 2^i + 1 这两个算式。这项研究也表明 “比较在希尔排序中是最主要的操作,而不是交换。” 用这样步长串行的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。

算法复杂度

  • 最差时间复杂度 O(nlog2 n)
  • 平均时间复杂度 依赖于步长间隔 O(nlog2 n)
  • 最优时间复杂度 O(nlogn)
  • 最差空间复杂度 O(n),辅助空间 O(1)

代码示例

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 };

      ShellSort(unsorted);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }

    static void ShellSort(int[] unsorted)
    {
      int gap = (int)Math.Ceiling(unsorted.Length / 2D);

      // start with the largest gap and work down to a gap of 1 
      while (gap > 0)
      {
        // do a gapped insertion sort for this gap size.
        // the first gap elements a[0..gap-1] are already in gapped order
        // keep adding one more element until the entire array is gap sorted 
        for (int i = 0; i < unsorted.Length; i++)
        {
          // add a[i] to the elements that have been gap sorted
          // save a[i] in temp and make a hole at position i
          int j = i;
          int temp = unsorted[i];

          // shift earlier gap-sorted elements up 
          // until the correct location for a[i] is found
          while (j >= gap && unsorted[j - gap] > temp)
          {
            unsorted[j] = unsorted[j - gap];
            j = j - gap;
          }

          // put temp (the original a[i]) in its correct location
          unsorted[j] = temp;
        }

        // change gap
        gap = (int)Math.Floor(0.5 + gap / 2.2);
      }
    }
  }

合并排序(Merge Sort)

合并排序是分治法的典型应用。

分治法(Divide-and-Conquer):将原问题划分成 n 个规模较小而结构与原问题相似的子问题;递归地解决这些问题,然后再合并其结果,就得到原问题的解。

分治模式在每一层上都有三个步骤:

  1. 分解(Divide):将原问题分解成一系列子问题;
  2. 解决(Conquer):递归地解决各个子问题。若子问题足够小,则直接求解;
  3. 合并(Combine):将子问题的结果合并成原问题的解。

合并排序算法完全依照了上述模式:

  1. 分解:将 n 个元素分成各含 n/2 个元素的子序列;
  2. 解决:用合并排序法对两个子序列递归地排序;
  3. 合并:合并两个已排序的子序列以得到排序结果。

jinglingshu_2014-07-21_14-40-37

算法描述

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针到达序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾;

算法复杂度

  • 最差时间复杂度 Θ(n*logn)
  • 平均时间复杂度 Θ(n*logn)
  • 最优时间复杂度 Θ(n)
  • 最差空间复杂度 Θ(n)

合并排序有着较好的渐进运行时间 Θ(nlogn),但其中的 merge 操作不是 in-place 操作。

示例代码

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 };

      int[] temp = new int[unsorted.Length];
      MergeSort(unsorted, 0, unsorted.Length, temp);
      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }

    static void MergeSort(int[] unsorted, int left, int right, int[] temp)
    {
      if (left + 1 < right)
      {
        // divide
        int mid = (left + right) / 2;

        // conquer
        MergeSort(unsorted, left, mid, temp);
        MergeSort(unsorted, mid, right, temp);

        // combine
        Merge(unsorted, left, mid, right, temp);
      }
    }

    static void Merge(int[] unsorted, int left, int mid, int right, int[] temp)
    {
      int leftPosition = left;
      int rightPosition = mid;
      int numberOfElements = 0;

      // merge two slots
      while (leftPosition < mid && rightPosition < right)
      {
        if (unsorted[leftPosition] < unsorted[rightPosition])
        {
          temp[numberOfElements++] = unsorted[leftPosition++];
        }
        else
        {
          temp[numberOfElements++] = unsorted[rightPosition++];
        }
      }

      // add remaining
      while (leftPosition < mid)
      {
        temp[numberOfElements++] = unsorted[leftPosition++];
      }
      while (rightPosition < right)
      {
        temp[numberOfElements++] = unsorted[rightPosition++];
      }

      // copy back
      for (int n = 0; n < numberOfElements; n++)
      {
        unsorted[left + n] = temp[n];
      }
    }
  }

堆排序(Heap Sort)

堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。二叉堆数据结构时一种数组对象,它可以被视为一棵完全二叉树。树中每个节点与数组中存放该节点值的那个元素对应。

二叉堆有两种,最大堆和最小堆。最大堆特性是指除了根以外的每个节点 i ,有 A(Parent(i)) ≥ A[i] ,即某个节点的值至多是和其父节点的值一样大。最小堆特性是指除了根以外的每个节点 i ,有 A(Parent(i)) ≤ A[i] ,最小堆的最小元素在根部。

在堆排序算法中,我们使用的是最大堆。最小堆通常在构造有限队列时使用。

堆可以被看成一棵树,节点在堆中的高度定义为从本节点到叶子的最长简单下降路径上边的数目;定义堆的高度为树根的高度。因为具有 n 个元素的堆是基于一棵完全二叉树,因而其高度为 Θ(lg n) 。

堆节点的访问

通常堆是通过一维数组来实现的。在数组起始为 0 的情形中,如果 i 为当前节点的索引,则有

  • 父节点在位置 floor((i-1)/2);
  • 左子节点在位置 (2*i+1);
  • 右子节点在位置 (2*i+2);

堆的操作

在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作:

  • 最大堆调整(Max-Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点,保持最大堆性质的关键。运行时间为 O(lg n)。
  • 创建最大堆(Build-Max-Heap):在无序的输入数组基础上构造出最大堆。运行时间为 O(n)。
  • 堆排序(HeapSort):对一个数组进行原地排序,卸载位在第一个数据的根节点,并做最大堆调整的递归运算。运行时间为 O(n*lg n)。
  • 抽取最大值(Extract-Max):相当于执行一次最大堆调整,最大值在根处。运行时间为 O(lg n)。

jinglingshu_2014-07-21_14-40-40

算法复杂度

  • 最差时间复杂度 O(n*logn)
  • 平均时间复杂度 Θ(n*logn)
  • 最优时间复杂度 O(n*logn)
  • 最差空间复杂度 O(n),辅助空间 O(1)

示例代码

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 4, 1, 5, 2, 6, 3, 7, 9, 8 };

      HeapSort(unsorted);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }

    static void HeapSort(int[] unsorted)
    {
      // build the heap in array so that largest value is at the root
      BuildMaxHeap(unsorted);

      // swap root node and the last heap node
      for (int i = unsorted.Length - 1; i >= 1; i--)
      {
        // array[0] is the root and largest value. 
        // the swap moves it in front of the sorted elements
        int max = unsorted[0];
        unsorted[0] = unsorted[i];
        unsorted[i] = max; // now, the largest one is at the end

        // the swap ruined the heap property, so restore it
        // the heap size is reduced by one
        MaxHeapify(unsorted, 0, i - 1);
      }
    }

    static void BuildMaxHeap(int[] unsorted)
    {
      // put elements of array in heap order, in-place
      // start is assigned the index in array of the last parent node
      // the last element in 0-based array is at index count-1; 
      // find the parent of that element
      for (int i = (unsorted.Length / 2) - 1; i >= 0; i--)
      {
        // sift down the node at index start to the proper place 
        // such that all nodes below the start index are in heap order
        MaxHeapify(unsorted, i, unsorted.Length - 1);
      }
      // after sifting down the root all nodes/elements are in heap order
    }

    static void MaxHeapify(int[] unsorted, int root, int bottom)
    {
      int rootValue = unsorted[root];
      int maxChild = root * 2 + 1; // start from left child

      // while the root has at least one child
      while (maxChild <= bottom)
      {
        // more children
        if (maxChild < bottom)
        {
          // if there is a right child and that child is greater
          if (unsorted[maxChild] < unsorted[maxChild + 1])
          {
            maxChild = maxChild + 1;
          }
        }

        // compare roots and the older children
        if (rootValue < unsorted[maxChild])
        {
          unsorted[root] = unsorted[maxChild];
          root = maxChild;

          // repeat to continue sifting down the child now
          maxChild = root * 2 + 1; // continue from left child
        }
        else
        {
          maxChild = bottom + 1;
        }
      }

      unsorted[root] = rootValue;
    }
  }

内省排序(Introspective Sort)

内省排序(Introsort)是由 David Musser 在 1997 年设计的排序算法。这个排序算法首先从快速排序开始,当递归深度超过一定深度(深度为排序元素数量的对数值)后转为堆排序。采用这个方法,内省排序既能在 常规数据集上实现快速排序的高性能,又能在最坏情况下仍保持 O(nlog n) 的时间复杂度。由于这两种算法都属于比较排序算法,所以内省排序也是一个比较排序算法。

在快速排序算法中,一个关键操作就是选择主元(Pivot):数列将被此主元位置分开成两部分。最简单的主元选择算法是使用第一个或者最后一个元素,但这在排列已部分有序的序列上性能很差。Niklaus Wirth 为此设计了一个快速排序的变体,使用处于中间的元素来防止在某些特定序列上性能退化为 O(n2) 的状况。该 median-of-3 选择算法从序列的第一、中间和最后一个元素取得中位数来作为主元。虽然这个算法在现实世界的数据上性能表现良好,但经过精心设计的序列仍能大幅降低此算法 性能。这样就有攻击者精心设计序列发送到因特网服务器以进行拒绝服务(DOS:Denial of Service)攻击的潜在可能性。

Musser 研究指出,在为 median-of-3 选择算法精心设计的 100,000 个元素序列上,内省排序算法的运行时间是快速排序的 1/200。在 Musser 的算法中,最终较小范围内数据的排序由 Robert Sedgewick 提出的小数据排序算法完成。

算法复杂度

  • 最差时间复杂度 O(n*log n)
  • 平均时间复杂度 O(n*log n)
  • 最优时间复杂度 O(n*log n)

微软 .NET 框架中 Array.Sort 方法的实现使用了内省排序(Introspective Sort)算法。

代码示例

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 
                         4, 1, 5, 2, 6, 3, 7, 9, 8, 10,
                         20, 11, 19, 12, 18, 17, 15, 16, 13, 14
                       };

      GenericQuickSorter<int>.DepthLimitedQuickSort(
        unsorted, 0, unsorted.Length - 1, 
        GenericQuickSorter<int>.QuickSortDepthThreshold);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }
  }

  internal class GenericQuickSorter<T>
      where T : IComparable<T>
  {
    internal const int QuickSortDepthThreshold = 32;

    internal static void DepthLimitedQuickSort(T[] keys, int left, int right, int depthLimit)
    {
      do
      {
        if (depthLimit == 0)
        {
          Heapsort(keys, left, right);
          return;
        }

        int i = left;
        int j = right;

        // pre-sort the low, middle (pivot), and high values in place.
        // this improves performance in the face of already sorted data, or 
        // data that is made up of multiple sorted runs appended together.
        int middle = i + ((j - i) >> 1);
        SwapIfGreater(keys, i, middle);  // swap the low with the mid point
        SwapIfGreater(keys, i, j);       // swap the low with the high
        SwapIfGreater(keys, middle, j);  // swap the middle with the high

        T x = keys[middle];
        do
        {
          while (keys[i].CompareTo(x) < 0) i++;
          while (x.CompareTo(keys[j]) < 0) j--;
          Contract.Assert(i >= left && j <= right,
            "(i>=left && j<=right)  Sort failed - Is your IComparer bogus?");

          if (i > j) break;
          if (i < j)
          {
            T key = keys[i];
            keys[i] = keys[j];
            keys[j] = key;
          }
          i++;
          j--;
        } while (i <= j);

        // The next iteration of the while loop is to 
        // "recursively" sort the larger half of the array and the
        // following calls recrusively sort the smaller half.  
        // So we subtrack one from depthLimit here so
        // both sorts see the new value.
        depthLimit--;

        if (j - left <= right - i)
        {
          if (left < j) DepthLimitedQuickSort(keys, left, j, depthLimit);
          left = i;
        }
        else
        {
          if (i < right) DepthLimitedQuickSort(keys, i, right, depthLimit);
          right = j;
        }
      } while (left < right);
    }

    private static void SwapIfGreater(T[] keys, int a, int b)
    {
      if (a != b)
      {
        if (keys[a] != null && keys[a].CompareTo(keys[b]) > 0)
        {
          T key = keys[a];
          keys[a] = keys[b];
          keys[b] = key;
        }
      }
    }

    private static void Heapsort(T[] keys, int lo, int hi)
    {
      Contract.Requires(keys != null);
      Contract.Requires(lo >= 0);
      Contract.Requires(hi > lo);
      Contract.Requires(hi < keys.Length);

      int n = hi - lo + 1;
      for (int i = n / 2; i >= 1; i = i - 1)
      {
        DownHeap(keys, i, n, lo);
      }
      for (int i = n; i > 1; i = i - 1)
      {
        Swap(keys, lo, lo + i - 1);
        DownHeap(keys, 1, i - 1, lo);
      }
    }

    private static void DownHeap(T[] keys, int i, int n, int lo)
    {
      Contract.Requires(keys != null);
      Contract.Requires(lo >= 0);
      Contract.Requires(lo < keys.Length);

      T d = keys[lo + i - 1];
      int child;
      while (i <= n / 2)
      {
        child = 2 * i;
        if (child < n
          && (keys[lo + child - 1] == null
            || keys[lo + child - 1].CompareTo(keys[lo + child]) < 0))
        {
          child++;
        }
        if (keys[lo + child - 1] == null
          || keys[lo + child - 1].CompareTo(d) < 0)
          break;
        keys[lo + i - 1] = keys[lo + child - 1];
        i = child;
      }
      keys[lo + i - 1] = d;
    }

    private static void Swap(T[] a, int i, int j)
    {
      if (i != j)
      {
        T t = a[i];
        a[i] = a[j];
        a[j] = t;
      }
    }
  }

.NET Array.Sort 实现

微软 .NET 框架中 Array.Sort 方法的实现使用了内省排序(Introspective Sort)算法:

这种实现也是不稳定的排序,也就是说,如果两个元素相等,则并不能保证它们的顺序,而相反一个稳定的排序算法则会保持相等元素原来的顺序。

如果数组的长度为 n ,则该实现的平均情况为 O(n log n) ,最坏情况为 O(n2)。

注:.NET 4.5 中使用这里描述的算法,而 .NET 4.0 及以前版本使用上面描述的快速排序和堆排序组合的内省排序算法。

class Program
  {
    static void Main(string[] args)
    {
      int[] unsorted = { 
                         4, 1, 5, 2, 6, 3, 7, 9, 8, 10,
                         20, 11, 19, 12, 18, 17, 15, 16, 13, 14
                       };

      GenericIntroSorter<int>.IntrospectiveSort(unsorted, 0, unsorted.Length);

      foreach (var key in unsorted)
      {
        Console.Write("{0} ", key);
      }

      Console.Read();
    }
  }

  internal class GenericIntroSorter<T>
      where T : IComparable<T>
  {
    internal const int IntrosortSwitchToInsertionSortSizeThreshold = 16;

    internal static void IntrospectiveSort(T[] keys, int left, int length)
    {
      Contract.Requires(keys != null);
      Contract.Requires(left >= 0);
      Contract.Requires(length >= 0);
      Contract.Requires(length <= keys.Length);
      Contract.Requires(length + left <= keys.Length);

      if (length < 2)
        return;

      IntroSort(keys, left, length + left - 1, 2 * FloorLog2(keys.Length));
    }

    private static void IntroSort(T[] keys, int lo, int hi, int depthLimit)
    {
      Contract.Requires(keys != null);
      Contract.Requires(lo >= 0);
      Contract.Requires(hi < keys.Length);

      while (hi > lo)
      {
        int partitionSize = hi - lo + 1;
        if (partitionSize <= IntrosortSwitchToInsertionSortSizeThreshold)
        {
          if (partitionSize == 1)
          {
            return;
          }
          if (partitionSize == 2)
          {
            SwapIfGreaterWithItems(keys, lo, hi);
            return;
          }
          if (partitionSize == 3)
          {
            SwapIfGreaterWithItems(keys, lo, hi - 1);
            SwapIfGreaterWithItems(keys, lo, hi);
            SwapIfGreaterWithItems(keys, hi - 1, hi);
            return;
          }

          InsertionSort(keys, lo, hi);
          return;
        }

        if (depthLimit == 0)
        {
          Heapsort(keys, lo, hi);
          return;
        }
        depthLimit--;

        int p = PickPivotAndPartition(keys, lo, hi);

        // Note we've already partitioned around the pivot 
        // and do not have to move the pivot again.
        IntroSort(keys, p + 1, hi, depthLimit);
        hi = p - 1;
      }
    }

    private static int PickPivotAndPartition(T[] keys, int lo, int hi)
    {
      Contract.Requires(keys != null);
      Contract.Requires(lo >= 0);
      Contract.Requires(hi > lo);
      Contract.Requires(hi < keys.Length);
      Contract.Ensures(Contract.Result<int>() >= lo && Contract.Result<int>() <= hi);

      // Compute median-of-three.  But also partition them.
      int middle = lo + ((hi - lo) / 2);

      // Sort lo, mid and hi appropriately, then pick mid as the pivot.
      SwapIfGreaterWithItems(keys, lo, middle);  // swap the low with the mid point
      SwapIfGreaterWithItems(keys, lo, hi);      // swap the low with the high
      SwapIfGreaterWithItems(keys, middle, hi);  // swap the middle with the high

      T pivot = keys[middle];
      Swap(keys, middle, hi - 1);

      // We already partitioned lo and hi and put the pivot in hi - 1.  
      // And we pre-increment & decrement below.
      int left = lo, right = hi - 1;

      while (left < right)
      {
        if (pivot == null)
        {
          while (left < (hi - 1) && keys[++left] == null) ;
          while (right > lo && keys[--right] != null) ;
        }
        else
        {
          while (pivot.CompareTo(keys[++left]) > 0) ;
          while (pivot.CompareTo(keys[--right]) < 0) ;
        }

        if (left >= right)
          break;

        Swap(keys, left, right);
      }

      // Put pivot in the right location.
      Swap(keys, left, (hi - 1));

      return left;
    }

    private static void Heapsort(T[] keys, int lo, int hi)
    {
      Contract.Requires(keys != null);
      Contract.Requires(lo >= 0);
      Contract.Requires(hi > lo);
      Contract.Requires(hi < keys.Length);

      int n = hi - lo + 1;
      for (int i = n / 2; i >= 1; i = i - 1)
      {
        DownHeap(keys, i, n, lo);
      }
      for (int i = n; i > 1; i = i - 1)
      {
        Swap(keys, lo, lo + i - 1);
        DownHeap(keys, 1, i - 1, lo);
      }
    }

    private static void DownHeap(T[] keys, int i, int n, int lo)
    {
      Contract.Requires(keys != null);
      Contract.Requires(lo >= 0);
      Contract.Requires(lo < keys.Length);

      T d = keys[lo + i - 1];
      int child;
      while (i <= n / 2)
      {
        child = 2 * i;
        if (child < n
          && (keys[lo + child - 1] == null
            || keys[lo + child - 1].CompareTo(keys[lo + child]) < 0))
        {
          child++;
        }
        if (keys[lo + child - 1] == null
          || keys[lo + child - 1].CompareTo(d) < 0)
          break;
        keys[lo + i - 1] = keys[lo + child - 1];
        i = child;
      }
      keys[lo + i - 1] = d;
    }

    private static void InsertionSort(T[] keys, int lo, int hi)
    {
      Contract.Requires(keys != null);
      Contract.Requires(lo >= 0);
      Contract.Requires(hi >= lo);
      Contract.Requires(hi <= keys.Length);

      int i, j;
      T t;
      for (i = lo; i < hi; i++)
      {
        j = i;
        t = keys[i + 1];
        while (j >= lo && (t == null || t.CompareTo(keys[j]) < 0))
        {
          keys[j + 1] = keys[j];
          j--;
        }
        keys[j + 1] = t;
      }
    }

    private static void SwapIfGreaterWithItems(T[] keys, int a, int b)
    {
      Contract.Requires(keys != null);
      Contract.Requires(0 <= a && a < keys.Length);
      Contract.Requires(0 <= b && b < keys.Length);

      if (a != b)
      {
        if (keys[a] != null && keys[a].CompareTo(keys[b]) > 0)
        {
          T key = keys[a];
          keys[a] = keys[b];
          keys[b] = key;
        }
      }
    }

    private static void Swap(T[] a, int i, int j)
    {
      if (i != j)
      {
        T t = a[i];
        a[i] = a[j];
        a[j] = t;
      }
    }

    private static int FloorLog2(int n)
    {
      int result = 0;
      while (n >= 1)
      {
        result++;
        n = n / 2;
      }
      return result;
    }
  }

参考资料

本篇文章《比较排序算法及复杂度分析》由 Dennis Gao 原创发表自博客园

转自:http://www.cnblogs.com/gaochundong/p/comparison_sorting_algorithms.html

转载请注明:jinglingshu的博客 » 比较排序算法及复杂度分析

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址