排序算法

Sorting Algotithms Collection 排序算法合集

0. 排序算法

排序算法在所有计算机算法,乃至整个计算机领域中,都占据着非常重要的地位。基础算法是软件的核心,而查找算法和排序算法则是计算机基础算法的核心。

排序算法是计算机科学中用于对元素序列进行排序的一系列算法。排序算法在实际应用中非常广泛,比如数据库索引、文件排序、数据检索等。

0.1 定义:

排序算法是一种将一组数据元素重新排列成有序序列的算法。这个“有序”可以是升序或降序。

0.2 作用:

排序算法在许多领域都有广泛的应用,包括但不限于:

•数据分析:对数据进行排序可以更容易地识别数据中的模式和趋势。

•数据库管理:数据库查询经常需要对结果进行排序。

•搜索算法:排序算法可以用于优化搜索过程,如二分搜索依赖于排序好的列表。

•算法实现:许多算法的实现依赖于排序,如归并排序是归并算法的基础。

0.3 分类:

  • 按算法的时间复杂度进行分类:

    • O(n^2) 算法:冒泡排序、选择排序、插入排序
    • O(n log n) 算法:快速排序、归并排序、堆排序
    • O(n) 算法:计数排序、桶排序、基数排序
  • 按照空间复杂度(内存使用量)进行分类:

    • 原地排序算法(空间复杂度为 O(1)):冒泡排序、选择排序、插入排序、堆排序
    • 非原地排序算法(空间复杂度大于 O(1)):归并排序、计数排序
  • 按照实现排序的方法进行分类:

    • 插入类排序:直接插入排序、二分插入排序、Shell排序(希尔排序)
    • 交换类排序:冒泡排序、快速排序、随机快速排序
    • 选择类排序:简单选择排序、堆排序
    • 归并类排序:归并排序
    • 分配类排序:计数排序、桶排序、基数排序
    • 混合类排序:鸡尾酒排序(冒泡排序的变体,双向冒泡)
    • 其他排序:拓扑排序、循环排序

1. Quick Sort 快速排序

快速排序(Quick Sort)是一种高效的排序算法,由C. A. R. Hoare在1960年提出。它采用分治法的策略,通过选择一个“基准”元素,将数组分为两部分:一部分包含所有小于基准的元素,另一部分包含所有大于基准的元素。然后递归地对这两部分进行快速排序。

1.1 算法步骤

•选择基准:从数组中选择一个元素作为基准。

•分区操作:重新排列数组,所有比基准小的元素放在基准的左边,所有比基准大的元素放在基准的右边。

•递归排序:递归地对基准左边和右边的子数组进行快速排序。

•完成:递归到基情况(数组只有一个或零个元素)时,排序完成。

1.2 算法图解

快速排序通过选择一个基准值,将数组分为两部分,然后递归地对这两部分进行排序。



BP Network

1.3 算法特点

•效率:在大多数情况下,快速排序的平均时间复杂度为O(n log n)。

•不稳定性:快速排序是不稳定的排序算法,因为在分区过程中可能会改变相同元素的相对顺序。

•时间复杂度:平均情况下为O(n log n),但在最坏情况下(例如,数组已经排序或完全逆序)为O(n^2)。

•空间复杂度:O(log n),快速排序是原地排序算法,但递归性质导致它需要O(log n)的额外栈空间。

1.4 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void quick_sort(vector<int>& nums, int l, int r) {
  if (l >= r) {
    return;
  }

  int first = l, last = r;
  while (l < r) {
    while (l < r && nums[l] < nums[first]) {
      l++;
    }
    while (l < r && nums[r] > nums[first]) {
      r--;
    }
    swap(nums[l], nums[r]);
  }
  swap(nums[first], nums[r]);
  quick_sort(nums, first, r - 1);
  quick_sort(nums, r + 1, last);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def quick_sort(arr, low, high):
  if low < high:
    # 分区操作
    pivot_index = partition(arr, low, high)
    # 递归地对基准左边和右边的子数组进行快速排序
    quick_sort(arr, low, pivot_index - 1)
    quick_sort(arr, pivot_index + 1, high)

def partition(arr, low, high):
  # 选择基准(这里以最右侧的元素作为基准)
  pivot = arr[high]
  i = low - 1
  for j in range(low, high):
    if arr[j] < pivot:
      i += 1
      arr[i], arr[j] = arr[j], arr[i]
  # 将基准放到正确的位置
  arr[i + 1], arr[high] = arr[high], arr[i + 1]
  return i + 1

# 示例
arr = [10, 7, 8, 9, 1, 5]
quick_sort(arr, 0, len(arr) - 1)
print("Sorted array is:", arr)

2. Merge Sort 归并排序

归并排序(Merge Sort)是一种分治算法,由约翰·冯·诺伊曼在1945年发明。它通过递归地将数组分成两半,然后对每一半进行排序,最后将排序好的两半合并在一起,从而完成整个数组的排序。

2.1 算法步骤

•分割:将待排序的数组分成两半,直到每个子数组只包含一个元素。

•递归排序:递归地对每个子数组进行归并排序。

•合并:将排序好的两个子数组合并成一个有序数组。

2.2 算法图解

归并排序通过递归地将数组分成更小的部分,然后合并这些部分,直到整个数组被排序。



BP Network



BP Network

2.3 算法特点

•稳定性:归并排序是一种稳定的排序算法,因为它不会改变相同元素的相对顺序。

•时间复杂度:无论最好、最坏还是平均情况下,时间复杂度都是O(n log n)。

•空间复杂度:O(n),归并排序需要使用到额外的空间来存储临时的合并结果。

2.4 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void print_arr(vector<int>& nums) {
  for (auto num:nums) {
    std::cout << num << " ";
  }
  std::cout << std::endl;
}

void merge(vector<int>& nums, int low, int mid, int high) {
  vector<int> tmp (high - low + 1);
  int i = low;
  int j = mid + 1;
  int k = 0;
  while (i <= mid && j <= high) {
    if (nums[i] < nums[j]) {
      tmp[k++] = nums[i++];
    } else {
      tmp[k++] = nums[j++];
    }
  }
  while (i <= mid) {
    tmp[k++] = nums[i++];
  }
  while (j <= high) {
    tmp[k++] = nums[j++];
  }
  for (int i = 0; i < tmp.size(); ++i) {
    nums[low+i] = tmp[i];
  }
}

void merge_sort(vector<int>& nums, int l, int r) {
  if (l == r) return;

  int mid = l + (r - l) / 2;
  merge_sort(nums, l, mid);
  merge_sort(nums, mid + 1, r);

  merge(nums, l, mid, r);
}

int main() {
  vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
  print_arr(arr);
  merge_sort(arr, 0, arr.size() - 1);
  print_arr(arr);
  return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Python
def merge_sort(arr):
  if len(arr) > 1:
    mid = len(arr) // 2  # 找到中间位置
    L = arr[:mid]  # 左侧序列
    R = arr[mid:]  # 右侧序列

    merge_sort(L)  # 递归对左侧序列进行排序
    merge_sort(R)  # 递归对右侧序列进行排序

    # 合并两个排序好的序列
    i = j = k = 0

    # 按顺序合并元素直到一个子数组为空
    while i < len(L) and j < len(R):
      if L[i] < R[j]:
        arr[k] = L[i]
        i += 1
      else:
        arr[k] = R[j]
        j += 1
      k += 1

    # 将剩余的元素移到数组的末尾
    while i < len(L):
      arr[k] = L[i]
      i += 1
      k += 1

    while j < len(R):
      arr[k] = R[j]
      j += 1
      k += 1

  return arr

# 示例
arr = [38, 27, 43, 3, 9, 82, 10]
sorted_arr = merge_sort(arr)
print("Sorted array is:", sorted_arr)

2.5 适用场景

归并排序由于其稳定性和时间复杂度,适用于以下情况: •大数据集:归并排序适合处理大型数据集,因为它的性能不会受到数据规模的影响。 •稳定性需求:当排序过程中保持元素的相对顺序很重要时,归并排序是一个合适的选择。 •外存排序:归并排序适合于磁盘等外存上的排序,因为它可以有效地减少读取和写入的次数。 •内存限制:尽管归并排序需要额外的内存空间,但通过优化可以实现为原地排序,从而减少内存使用。

3. Insertion Sort 插入排序

插入排序(Insertion Sort)是一种简单直观的排序算法,它通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1 算法步骤

•开始:假设序列中第一个元素已经排序;

•插入:取下一个未排序的元素,与已排序序列中的元素从后向前比较;

•移动:如果已排序序列中的元素比当前元素大,则将该元素移到下一位;

•插入:将当前元素放到合适的位置;

•重复:对序列中剩余的每个元素重复步骤2-4。

3.2 算法图解

插入排序通过将每个元素插入到前面已排序序列中的适当位置,从而逐步构建完整的有序序列。



BP Network

动态示意图:



插入排序示意图

3.3 算法特点

•简单:插入排序的原理和实现都很简单,容易理解和编程实现。

•稳定:插入排序是一种稳定的排序算法,因为它不会改变相同元素的相对顺序。

•时间复杂度:在最好的情况下(即数列已经是排序状态),时间复杂度为O(n);在最坏和平均情况下,时间复杂度为O(n^2)。

•空间复杂度:O(1),插入排序是原地排序,不需要额外的存储空间。

3.4 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def insertion_sort(arr):
  # 从第二个元素开始遍历,因为第一个元素可以认为已经排序
  for i in range(1, len(arr)):
    key = arr[i]
    # 从当前元素的前一个元素开始,向前遍历已排序的元素
    j = i - 1
    # 将大于key的元素向后移动
    while j >= 0 and key < arr[j]:
        arr[j + 1] = arr[j]
        j -= 1
    # 将key放到正确的位置
    arr[j + 1] = key
  return arr

# 示例
arr = [12, 11, 13, 5, 6]
sorted_arr = insertion_sort(arr)
print("Sorted array is:", sorted_arr)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void insertion_sort(vector<int>& nums) {
  int n = nums.size();
  for (int i = 1; i < n; ++i) {
    int key = nums[i];
    int j = i - 1;
    while (j >= 0 && nums[j] > key) {
      nums[j+1] = nums[j];
      j--;
    }
    nums[j+1] = key;
  }
}

3.5 适用场景

插入排序虽然在最坏情况下效率不高,但在以下情况下可能很有用:

•数据规模较小:对于小型数据集,插入排序的性能是可接受的。

•初始数据部分有序:如果数据集已经部分有序,插入排序的性能会接近线性。

•教学目的:由于其简单性,插入排序常被用作教学示例,帮助初学者理解算法和排序的基本概念。

•在线排序:插入排序适合在线排序,即数据逐个到来时进行排序。

4. 希尔排序(Shell Sorting)

希尔排序是插入排序的一种更高效的改进版本,也称为缩小增量排序。它通过引入一个“增量”的概念来优化插入排序,允许进行较远距离的元素交换,从而减少比较和移动的次数。

4.1 算法步骤

•开始:选择一个增量序列,它可以是固定的,也可以是动态生成的。

•排序:使用插入排序对增量序列定义的子序列进行排序。

•缩小:减小增量序列的值,重复上一步,直到增量为1。

•完成:当增量为1时,整个数组将被排序。

4.2 算法图解

希尔排序通过将原始数据分成多个子序列,每个子序列的元素之间相隔特定的增量。然后对每个子序列进行插入排序,随着增量的减小,子序列的间隔也逐渐减小,直到增量为1,此时整个数组已经接近有序,最终进行一次普通的插入排序即可完成排序。



BP Network

4.3 算法特点

•效率:希尔排序的效率依赖于增量序列的选择,通常比普通插入排序要快。

•稳定性:希尔排序是不稳定的排序算法,因为它在插入排序过程中可能会改变相同元素的相对顺序。

•时间复杂度:希尔排序的平均时间复杂度为O(n^1.5)到O(n^2),这取决于增量序列的选择。在最好的情况下,时间复杂度可以接近O(n log n)。

•空间复杂度:O(1),希尔排序是原地排序,不需要额外的存储空间。

4.4 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void print_arr(vector<int>& nums) {
  for (auto num:nums) {
    std::cout << num << " ";
  }
  std::cout << std::endl;
}

void shell_sort(vector<int>& nums) {
  int n = nums.size();
  int len = n / 2;
  while (len > 0) {
    for (int i = len; i < n; i++) {
      int j = i;
      int temp = nums[i];
      while (j - len >= 0 && temp < nums[j - len]) {
        nums[j] = nums[j - len];
        j -= len;
      }
      nums[j] = temp;
    }
    len = len / 2;
  }
  return;
}

int main() {
  vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
  print_arr(arr);
  shell_sort(arr);
  print_arr(arr);
  return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def shell_sort(arr):
  n = len(arr)
  gap = n // 2  # 初始增量
  while gap > 0:
    for i in range(gap, n):
      temp = arr[i]
      j = i
      # 对子序列进行插入排序
      while j >= gap and arr[j - gap] > temp:
        arr[j] = arr[j - gap]
        j -= gap
      arr[j] = temp
    gap //= 2  # 减小增量
  return arr

# 示例
arr = [12, 11, 13, 5, 6, 7, 10, 9]
sorted_arr = shell_sort(arr)
print("Sorted array is:", sorted_arr)

4.5 适用场景

希尔排序由于其较好的性能,适用于以下情况:

•中等数据规模:对于数据规模不是非常大的情况,希尔排序可以提供比普通插入排序更好的性能。

•初始数据部分有序:如果数据部分有序,希尔排序的性能会有所提升。

•增量序列选择:通过精心选择增量序列,希尔排序可以接近于O(n log n)的时间复杂度,从而在某些情况下比快速排序等算法更高效。

5. Bubble Sort 冒泡排序

冒泡排序(Bubble Sort)是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复进行直到没有再需要交换,也就是说该数列已经排序完成。

5.1 算法步骤

•开始:从数列的第一个元素开始,比较相邻的两个元素。

•比较与交换:如果左边的元素大于右边的元素,就交换它们两个。

•移动:移动到下一个元素对,重复步骤2。

•重复:继续这个过程,直到最后一次交换发生,此时数列的最后一个元素是最大的,已经被“冒泡”到它应该在的位置。

•减少比较次数:由于最大的元素已经在它应在的位置,所以下一次遍历可以减少一个比较(即从第一个元素开始,不需要再和它比较)。

5.2 算法图解

冒泡排序从头开始,依次比较数组中相邻的2个元素,如果后面的数比前面的数大,则交换2个数,否则不交换。每进行一轮比较,都会把数组中最大的元素放到最后面。



BP Network

5.3 算法特点

  • 简单:冒泡排序的原理简单,容易实现。
  • 稳定:冒泡排序是一种稳定的排序算法,因为它不会改变相同元素之间的顺序。
  • 时间复杂度:平均和最坏时间复杂度均为O(n^2),其中n是数列的长度。在最好的情况下(即数列已经是排序状态),时间复杂度为O(n)。
  • 空间复杂度:O(1),因为冒泡排序是原地排序,不需要额外的存储空间。

5.4 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def bubble_sort(arr):
  n = len(arr)
  swapped = False
  for i in range(n):
      # 由于每次最大的元素都会被放到它应在的位置,所以可以减少比较次数
    swapped = False
    for j in range(0, n-i-1):
      # 相邻元素两两比较
      if arr[j] > arr[j+1]:
        # 发现元素顺序错误,交换它们
        arr[j], arr[j+1] = arr[j+1], arr[j]
        swapped = True
    if not flag:
        break
  return arr

# 示例
arr = [64, 34, 25, 12, 22, 11, 90]
sorted_arr = bubble_sort(arr)
print("Sorted array is:", sorted_arr)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void print_arr(vector<int>& nums) {
  for (auto num:nums) {
    std::cout << num << " ";
  }
  std::cout << std::endl;
}

void bubble_sort(vector<int>& nums) {
  int n = nums.size();
  bool swapped = false;
  for (int i = 0; i < n; ++i) {
    swapped = false;
    for (int j = 0; j < n - i - 1; ++j) {
      if (nums[j] > nums[j+1]) {
        swap(nums[j], nums[j+1]);
        swapped = true;
      }
    }
    if (!swapped) { // 一旦没有交换操作,说明已经完成排序,可以跳出循环
      break;
    }
  }
}

int main() {
  vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
  print_arr(arr);
  bubble_sort(arr);
  print_arr(arr); // 11 12 22 25 34 64 90
  return 0;
}

5.5 使用场景

冒泡排序由于其性能原因,通常不适用于大型数据集的排序。然而,它在以下情况下可能很有用:

  • 数据规模较小:当数据集很小或者几乎已经排序时,冒泡排序的性能是可接受的。
  • 教学目的:由于其简单性,冒泡排序常被用作教学示例,帮助初学者理解算法和排序的基本概念。
  • 需要稳定性:在某些特定情况下,保持元素的相对顺序很重要,冒泡排序可以满足这种稳定性需求。

6. 选择排序

选择排序是一种简单直观的排序算法,它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

6.1 算法步骤

•开始:在未排序序列中找到最小(大)元素;

•交换:将找到的最小(大)元素与序列的第0个元素交换;

•移动:从序列的第1个元素开始,继续寻找最小(大)元素,然后与序列的第1个元素交换;

•重复:重复步骤2和3,直到序列的第n-1个元素(其中n是序列的长度)。

6.2 算法图解

选择排序通过重复扫描数组,找到最小的元素,然后将其与当前位置的元素交换。这个过程会一直进行,直到整个数组被排序。



BP Network

6.3 算法特点

•简单:选择排序的实现相对简单,容易理解和编程实现。

•不稳定:选择排序在交换过程中可能会改变相同元素的顺序,因此它不是稳定的排序算法。

•时间复杂度:无论最好、最差还是平均情况下,时间复杂度都是O(n^2),其中n是数列的长度。

•空间复杂度:O(1),选择排序是原地排序,不需要额外的存储空间。

6.4 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def selection_sort(arr):
  n = len(arr)
  for i in range(n-1):
    # 找到最小元素的索引
    min_idx = i
    for j in range(i+1, n):
      if arr[j] < arr[min_idx]:
          min_idx = j
    # 将找到的最小元素交换到序列的前面
    arr[i], arr[min_idx] = arr[min_idx], arr[i]
  return arr

# 示例
arr = [64, 34, 25, 12, 22, 11, 90]
sorted_arr = selection_sort(arr)
print("Sorted array is:", sorted_arr)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void print_arr(vector<int>& nums) {
  for (auto num:nums) {
    std::cout << num << " ";
  }
  std::cout << std::endl;
}

void selection_sort(vector<int>& nums) {
  int mid_idx;
  int n = nums.size();
  for (int i = 0; i < n - 1; ++i) {
    mid_idx = i;
    for (int j = i + 1; j < n; ++j) {
      if (nums[j] < nums[mid_idx]) {
        mid_idx = j;
      }
    }
    swap(nums[mid_idx], nums[i]);
  }
}

int main() {
  vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
  print_arr(arr);
  selection_sort(arr);
  print_arr(arr);
  return 0;
}

6.5 适用场景

选择排序的性能相对较差,因此它不适用于大型数据集的排序。然而,在以下情况下,选择排序可能比较适用:

•数据规模较小:当数据集较小时,选择排序的简单性可能使其成为一个合适的选择。

•教学目的:由于其实现简单,选择排序常被用作教学示例,帮助初学者理解算法和排序的基本概念。

•排序过程中的特定操作:在某些特定的应用场景中,如果排序过程中需要频繁地访问未排序部分的元素,选择排序可能比其他算法更合适。。

6. 堆排序

7. 计数排序

8. 桶排序

Reference:

[1]. https://mp.weixin.qq.com/s/P8MmmMc4vB_I9tnK3towLQ

Buy me a coffee~
支付宝
微信
0%