Unverified Commit 9ed0f750 authored by Shuhao Zhang's avatar Shuhao Zhang Committed by GitHub
Browse files

Merge branch 'master' into patch-4

parents c492a4a6 7f521d3e
Loading
Loading
Loading
Loading
+65 −3
Original line number Diff line number Diff line
对所有记录建 [](../ds/heap.md) 
本页面将简要介绍堆排序

依次取出堆顶元素,就可以得到排好序的序列。
## 简介

时间复杂度为 $O(n\log n)$ 。
堆排序(英语:Heapsort)是指利用 [](../ds/heap.md) 这种数据结构所设计的一种排序算法。堆排序的适用数据结构为数组。

## 工作原理

它的工作原理为对所有待排序元素建堆,然后依次取出堆顶元素,就可以得到排好序的序列。

当当前的结点下标为 `i` 时,父结点、左子结点和右子结点的选择方式如下:

```cpp
//这里 floor 函数将实数映射到最小的前导整数。
iParent(i) = floor((i - 1) / 2);
iLeftChild(i) = 2 * i + 1;
iRightChild(i) = 2 * i + 2;
```

## 性质

### 稳定性

堆排序是一种不稳定的排序方法。

### 时间复杂度

堆排序的平均时间复杂度、最优时间复杂度、最坏时间复杂度均为 $O(n\log n)$ 。

## 代码实现

### C++

```cpp
void max_heapify(int arr[], int start, int end) {
  // 建立父结点指标和子结点指标
  int dad = start;
  int son = dad * 2 + 1;
  while (son <= end) {  // 子结点指标在范围内才做比较
    if (son + 1 <= end &&
        arr[son] < arr[son + 1])  // 先比较两个子结点大小,选择最大的
      son++;
    if (arr[dad] >
        arr[son])  // 如果父结点比子结点大,代表调整完毕,直接跳出函数
      return;
    else {  // 否则交换父子内容,子结点再和孙结点比较
      swap(arr[dad], arr[son]);
      dad = son;
      son = dad * 2 + 1;
    }
  }
}

void heap_sort(int arr[], int len) {
  // 初始化,i 从最后一个父结点开始调整
  for (int i = len / 2 - 1; i >= 0; i--) max_heapify(arr, i, len - 1);
  // 先将第一个元素和已经排好的元素前一位做交换,再重新调整(刚调整的元素之前的元素),直到排序完毕
  for (int i = len - 1; i > 0; i--) {
    swap(arr[0], arr[i]);
    max_heapify(arr, 0, i - 1);
  }
}
```

## 外部链接

-  [堆排序 - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/%E5%A0%86%E6%8E%92%E5%BA%8F) 
+6 −10
Original line number Diff line number Diff line
@@ -4,8 +4,6 @@

快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),简称快排,是一种被广泛运用的排序算法。

快速排序是 C++ 标准库中 `std::sort` 的实现算法。[^ref1]

## 工作原理

快速排序的工作原理是通过 [分治](./divide-and-conquer.md) 的方式来将一个数组排序。
@@ -42,13 +40,13 @@

快速排序的最佳时间复杂度和平均时间复杂度为 $O(n\log n)$ ,最坏时间复杂度为 $O(n^2)$ 。

实践中几乎不可能达到最坏情况,且因为快速排序的内存访问遵循局部性原理,多数情况下快速排序的表现大幅优于堆排序等其他复杂度为 $O(n \log n)$ 的排序算法。[^ref2]
实践中几乎不可能达到最坏情况,且因为快速排序的内存访问遵循局部性原理,多数情况下快速排序的表现大幅优于堆排序等其他复杂度为 $O(n \log n)$ 的排序算法。[^ref1]

在选择 $m$ 的过程中,使用 [Median of Medians](https://en.wikipedia.org/wiki/Median_of_medians) 算法就可以保证最坏时间复杂度为 $O(n\log n)$ 。但是其过于复杂,实践中一般不使用。

## 代码实现

### C++[^ref3]
### C++[^ref2]

```cpp
struct Range {
@@ -83,7 +81,7 @@ void quick_sort(T arr[], const int len) {
}
```

## 内省排序[^ref4]
## 内省排序[^ref3]

内省排序(introspective sort)是快速排序和 [堆排序](heap-sort.md) 的结合,由 David Musser 于 1997 年发明。内省排序其实是对快速排序的一种优化,保证了最差时间复杂度为 $O(n\log n)$ 。

@@ -103,10 +101,8 @@ void quick_sort(T arr[], const int len) {

## 参考资料与注释

[^ref1]:  [What algorithms are used in C++11 std::sort in different STL implementations?](https://stackoverflow.com/questions/22339240/what-algorithms-are-used-in-c11-stdsort-in-different-stl-implementations) 

[^ref2]:  [C++ 性能榨汁机之局部性原理 - I'm Root lee !](http://irootlee.com/juicer_locality/) 
[^ref1]:  [C++ 性能榨汁机之局部性原理 - I'm Root lee !](http://irootlee.com/juicer_locality/) 

[^ref3]:  [算法实现/排序/快速排序 - 维基教科书,自由的教学读本](https://zh.wikibooks.org/wiki/%E7%AE%97%E6%B3%95%E5%AE%9E%E7%8E%B0/%E6%8E%92%E5%BA%8F/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F) 
[^ref2]:  [算法实现/排序/快速排序 - 维基教科书,自由的教学读本](https://zh.wikibooks.org/wiki/%E7%AE%97%E6%B3%95%E5%AE%9E%E7%8E%B0/%E6%8E%92%E5%BA%8F/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F) 

[^ref4]:  [introsort](https://en.wikipedia.org/wiki/Introsort) 
[^ref3]:  [introsort](https://en.wikipedia.org/wiki/Introsort) 
+2 −4
Original line number Diff line number Diff line
## 跳表

跳表(Skip List)是由 William Pugh 发明的一种查找数据结构,支持对数据的快速查找,插入和删除。

跳表的期望空间复杂度为 $O(n)$ ,跳表的查询,插入和删除操作的期望时间复杂度都为 $O(\log n)$ 。
@@ -20,7 +18,7 @@

### 空间复杂度

对于一个节点节点而言,节点的最高层数为 $i$ 的概率为 $p^{i-1}(1 - p)$ 。所以,跳表的期望层数为 $\sum_{i>=1} ip^{i - 1}(1-p) = \frac{1}{1 - p}$ ,且因为 $p$ 为常数,所以跳表的 **期望空间复杂度** 为 $O(n)$ 。
对于一个节点而言,节点的最高层数为 $i$ 的概率为 $p^{i-1}(1 - p)$ 。所以,跳表的期望层数为 $\sum_{i>=1} ip^{i - 1}(1-p) = \frac{1}{1 - p}$ ,且因为 $p$ 为常数,所以跳表的 **期望空间复杂度** 为 $O(n)$ 。

在最坏的情况下,每一层有序链表等于初始有序链表,即跳表的 **最差空间复杂度** 为 $O(n \log n)$ 。

@@ -160,7 +158,7 @@ bool erase(const K &key) {
  // 节点不存在
  if (p->key != key) return false;

  // 从最底层开始删
  // 从最底层开始删
  for (int i = 0; i <= level; ++i) {
    // 如果这层没有p删除就完成了
    if (update[i]->forward[i] != p) {
+0 −2
Original line number Diff line number Diff line
@@ -152,5 +152,3 @@ switch (i) {

??? note "如何理解 switch"
    在上文中,用了大量“case 分句”,“case 子句”等用语,实际上,在底层实现中,switch 相当于一组跳转语句。也因此,有 Duff's Device 这种奇技淫巧,希望了解的人可以自行学习。
    
    另外还有一种理解 switch 的方式,就是从其逻辑意义去理解。在这种理解里,case 代表的就是一组子句,而 switch 根据选择句的值选择某个 case 分句进行执行。在这种理解中,除了极少数例外,case 是必须要加 break 和花括号的。
+2 −2
Original line number Diff line number Diff line
@@ -437,9 +437,9 @@ inline void _debug(const char* format, First first, Rest... rest) {
}

template <typename T>
ostream& operator<<(ostream& os, vector<T> V) {
ostream& operator<<(ostream& os, const vector<T>& V) {
  os << "[ ";
  for (auto vv : V) os << vv << ", ";
  for (const auto& vv : V) os << vv << ", ";
  os << "]";
  return os;
}
Loading