当前位置: 首页 > news >正文

插入排序、归并排序、堆排序和快速排序的稳定性分析

插入排序、归并排序、堆排序和快速排序的稳定性分析

  • 一、插入排序的稳定性
  • 二、归并排序的稳定性
  • 三、堆排序的稳定性
  • 四、快速排序的稳定性
  • 总结

在计算机科学中,排序是将一组数据按照特定顺序进行排列的过程。排序算法的效率和稳定性是评价其优劣的两个重要指标。稳定性指的是在排序过程中,相等的元素在排序后保持原有的相对顺序。本文将详细分析插入排序、归并排序、堆排序和快速排序这四种常见排序算法的稳定性,并通过浅显易懂的语言进行解释。
在这里插入图片描述

一、插入排序的稳定性

插入排序是一种简单直观的排序算法,它的工作原理类似于我们整理扑克牌的过程。在插入排序中,我们将待排序的元素逐个插入到已排序的序列中,从而得到一个新的有序序列。由于插入排序在每次插入元素时,都会保证已排序序列的有序性,因此它是稳定的排序算法。

具体来说,插入排序从第一个元素开始,认为该元素已经被排序;取出下一个元素,在已经排序的元素序列中从后向前扫描;如果该元素(已排序)大于新元素,将该元素移到下一位置;重复步骤,直到找到已排序的元素小于或者等于新元素的位置;将新元素插入到该位置后。在插入过程中,如果遇到相等的元素,插入排序会将新元素插入到相等元素的后面,从而保证了稳定性。

伪代码示例:

for i from 1 to n-1 do  key = A[i]  j = i - 1  while j >= 0 and A[j] > key do  A[j+1] = A[j]  j = j - 1  end while  A[j+1] = key  
end for

C代码示例:

void insertionSort(int A[], int n) {  int i, j, key;  for (i = 1; i < n; i++) {  key = A[i];  j = i - 1;  while (j >= 0 && A[j] > key) {  A[j + 1] = A[j];  j = j - 1;  }  A[j + 1] = key;  }  
}

二、归并排序的稳定性

归并排序是一种采用分治思想的排序算法。它将待排序序列划分为若干个子序列,每个子序列是有序的;然后再将这些有序子序列逐步合并,最终得到一个完全有序的序列。归并排序在合并过程中,如果遇到相等的元素,会将它们按照原有的相对顺序进行合并,因此归并排序是稳定的排序算法。

具体来说,归并排序首先将待排序序列划分为若干个子序列,每个子序列只包含一个元素,因此它们都是有序的。然后,通过两两合并这些有序子序列,得到更长的有序序列。在合并过程中,如果遇到相等的元素,归并排序会将它们按照原有的相对顺序进行合并。这样,在最终得到的有序序列中,相等元素的相对顺序与原始序列中的相对顺序保持一致,从而保证了稳定性。
伪代码示例:

function mergeSort(A, low, high)  if low < high then  mid = (low + high) / 2  mergeSort(A, low, mid)  mergeSort(A, mid+1, high)  merge(A, low, mid, high)  end if  
end function  function merge(A, low, mid, high)  n1 = mid - low + 1  n2 = high - mid  create arrays Left[n1] and Right[n2]  for i from 0 to n1 do  Left[i] = A[low + i]  end for  for j from 0 to n2 do  Right[j] = A[mid + 1 + j]  end for  i = 0  j = 0  k = low  while i < n1 and j < n2 do  if Left[i] <= Right[j] then  A[k] = Left[i]  i = i + 1  else  A[k] = Right[j]  j = j + 1  end if  k = k + 1  end while  while i < n1 do  A[k] = Left[i]  i = i + 1  k = k + 1  end while  while j < n2 do  A[k] = Right[j]  j = j + 1  k = k + 1  end while  
end function

C代码示例:

void merge(int A[], int low, int mid, int high) {  int i, j, k;  int n1 = mid - low + 1;  int n2 = high - mid;  int Left[n1], Right[n2];  for (i = 0; i < n1; i++)  Left[i] = A[low + i];  for (j = 0; j < n2; j++)  Right[j] = A[mid + 1 + j];  i = 0;  j = 0;  k = low;  while (i < n1 && j < n2) {  if (Left[i] <= Right[j]) {  A[k] = Left[i];  i++;  } else {  A[k] = Right[j];  j++;  }  k++;  }  while (i < n1) {  A[k] = Left[i];  i++;  k++;  }  while (j < n2) {  A[k] = Right[j];  j++;  k++;  }  
}  void mergeSort(int A[], int low, int high) {  if (low < high) {  int mid = low + (high - low) / 2;  mergeSort(A, low, mid);  mergeSort(A, mid + 1, high);  merge(A, low, mid, high);  }  
}

三、堆排序的稳定性

堆排序是一种基于堆数据结构的排序算法。堆是一种特殊的树形数据结构,每个节点都有一个值,且整棵树满足堆的性质:即父节点的值大于或等于(或小于或等于)其子节点的值。堆排序通过构建最大堆或最小堆,然后交换堆顶元素与末尾元素并调整堆的结构,最终得到一个有序序列。然而,堆排序在调整堆的过程中可能会改变相等元素之间的相对顺序,因此堆排序不是稳定的排序算法。

具体来说,在堆排序过程中,当遇到相等的元素时,由于堆的调整过程可能会改变它们之间的相对顺序,因此无法保证排序后的序列中相等元素的相对顺序与原始序列中的相对顺序一致。因此,堆排序不是稳定的排序算法。
伪代码示例:

function heapSort(A)  n = length(A)  buildMaxHeap(A)  for i from n-1 downto 1 do  swap A[0] and A[i]  maxHeapify(A, 0, i-1)  end for  
end function  function buildMaxHeap(A)  n = length(A)  for i from floor(n/2)-1 downto 0 do  maxHeapify(A, i, n-1)  end for  
end function  function maxHeapify(A, i, heapSize)  left = 2*i + 1  right = 2*i + 2  largest = i  if left < heapSize and A[left] > A[largest] then  largest = left  end if  if right < heapSize and A[right] > A[largest] then  largest = right  end if  if largest != i then  swap A[i] and A[largest]  maxHeapify(A, largest, heapSize)  end if  
end function

C代码示例:

void heapify(int A[], int n, int i) {  int largest = i;  int left = 2 * i + 1;  int right = 2 * i + 2;  if (left < n && A[left] > A[largest])  largest = left;  if (right < n && A[right] > A[largest])  largest = right;  if (largest != i) {  int swap = A[i];  A[i] = A[largest];  A[largest] = swap;  heapify(A, n, largest);  }  
}  void heapSort(int A[], int n) {  for (int i = n / 2 - 1; i >= 0; i--)  heapify(A, n, i);  for (int i = n - 1; i >= 0; i--) {  int temp = A[0];  A[0] = A[i];  A[i] = temp;  heapify(A, i, 0);  }  
}

四、快速排序的稳定性

快速排序是一种高效的排序算法,它采用分治的思想进行排序。快速排序通过选择一个基准元素将待排序序列划分为两个子序列,一个子序列中的元素都小于基准元素,另一个子序列中的元素都大于基准元素;然后递归地对这两个子序列进行快速排序,最终得到一个有序序列。然而,快速排序在划分过程中可能会改变相等元素之间的相对顺序,因此快速排序不是稳定的排序算法。

具体来说,在快速排序过程中,当遇到相等的元素时,由于划分过程可能会将它们分到不同的子序列中,从而导致它们之间的相对顺序发生改变。因此,快速排序无法保证排序后的序列中相等元素的相对顺序与原始序列中的相对顺序一致。因此,快速排序不是稳定的排序算法。
伪代码示例:

function quickSort(A, low, high)  if low < high then  pi = partition(A, low, high)  quickSort(A, low, pi-1)  quickSort(A, pi+1, high)  end if  
end function  function partition(A, low, high)  pivot = A[high]  i = low - 1  for j from low to high-1 do  if A[j] <= pivot then  i = i + 1  swap A[i] and A[j]  end if  end for  swap A[i+1] and A[high]

C代码示例

#include <stdio.h>  // 交换数组中的两个元素  
void swap(int* a, int* b) {  int temp = *a;  *a = *b;  *b = temp;  
}  // 分区函数,返回分区后基准元素的位置  
int partition(int A[], int low, int high) {  int pivot = A[high]; // 选择最右边的元素作为基准  int i = (low - 1); // 指向最小元素的指针  for (int j = low; j <= high - 1; j++) {  // 如果当前元素小于或等于基准元素  if (A[j] <= pivot) {  i++; // 增加指针  swap(&A[i], &A[j]); // 交换元素  }  }  swap(&A[i + 1], &A[high]); // 将基准元素放到正确的位置  return (i + 1); // 返回基准元素的位置  
}  // 快速排序函数  
void quickSort(int A[], int low, int high) {  if (low < high) {  int pi = partition(A, low, high); // 获取基准元素的位置  // 递归地对基准元素左边和右边的子数组进行排序  quickSort(A, low, pi - 1);  quickSort(A, pi + 1, high);  }  
}  // 打印数组的函数  
void printArray(int A[], int size) {  for (int i = 0; i < size; i++) {  printf("%d ", A[i]);  }  printf("\n");  
}  // 主函数,测试快速排序  
int main() {  int A[] = {10, 7, 8, 9, 1, 5};  int n = sizeof(A) / sizeof(A[0]);  printf("Original array: \n");  printArray(A, n);  quickSort(A, 0, n - 1);  printf("Sorted array: \n");  printArray(A, n);  return 0;  
}

这段代码定义了一个快速排序函数quickSort,一个分区函数partition,以及一个用于交换数组中两个元素的辅助函数swap。主函数main中创建了一个待排序的数组,并调用quickSort函数对其进行排序。排序完成后,使用printArray函数打印排序后的数组。

需要注意的是,快速排序在最坏情况下的时间复杂度为O(n^2),这通常发生在输入数组已经排序或接近排序的情况下。为了避免这种情况,可以采取一些策略,如随机选择基准元素或使用三数取中法来选择基准元素。然而,在平均情况下,快速排序的时间复杂度为O(n log n),并且由于其原地排序的特性(不需要额外的存储空间),它在实践中被广泛使用

总结

综上所述,插入排序和归并排序是稳定的排序算法,而堆排序和快速排序不是稳定的排序算法。在实际应用中,我们需要根据具体的需求和数据特征选择合适的排序算法。如果稳定性是首要考虑的因素,那么插入排序和归并排序将是更好的选择;如果对排序速度有较高要求且可以容忍一定程度的不稳定性,那么堆排序和快速排序也是值得考虑的选项。

需要注意的是,虽然本文重点分析了这四种排序算法的稳定性,但在实际应用中还需要考虑其他因素,如算法的时间复杂度、空间复杂度以及数据规模等。只有综合考虑这些因素,才能选择出最适合具体应用场景的排序算法。

此外,对于稳定性的理解也需要注意一些细节。稳定性并不是所有情况下都需要考虑的因素,有些应用场景可能并不关心排序算法是否稳定。因此,在选择排序算法时,我们需要根据具体的应用场景和需求来进行权衡和选择。

最后,需要强调的是,排序算法的选择和使用是一个非常重要的计算机科学问题。在实际应用中,我们需要根据具体的问题和数据特征来选择合适的排序算法,并进行必要的优化和调整。只有这样,才能充分发挥排序算法的优势,提高程序的效率和性能。

相关文章:

  • Kubernetes之Projected Volume
  • 物理寻址和功能寻址,服务器不同的应答策略和NRC回复策略
  • 微信小程序页面生命周期和小程序api组件的生命周期
  • 算法刷题笔记(3.25-3.29)
  • 【Git项目部署到本地仓库】
  • 2024-03-28 Java8之Collectors类
  • MybatisPlus速成
  • Hive查询转换与Hadoop生态系统引擎与优势
  • python---基础(一)
  • 发生播放错误,即将重试 jellyfin
  • 集合框架——Map
  • MySQL索引特性
  • 备考ICA----Istio实验12---配置双向TLS Istio Ingress Gateway实验
  • 【Web自动化】Selenium的使用(一)
  • C# OpenCvSharp 轮廓检测
  • [case10]使用RSQL实现端到端的动态查询
  • exports和module.exports
  • input的行数自动增减
  • JS变量作用域
  • mysql innodb 索引使用指南
  • PHP 程序员也能做的 Java 开发 30分钟使用 netty 轻松打造一个高性能 websocket 服务...
  • 深入浅出Node.js
  • 使用common-codec进行md5加密
  • 王永庆:技术创新改变教育未来
  • 用Canvas画一棵二叉树
  • 宾利慕尚创始人典藏版国内首秀,2025年前实现全系车型电动化 | 2019上海车展 ...
  • ​queue --- 一个同步的队列类​
  • ​水经微图Web1.5.0版即将上线
  • (2)Java 简介
  • (4)Elastix图像配准:3D图像
  • (6)STL算法之转换
  • (C++)八皇后问题
  • (c语言)strcpy函数用法
  • (多级缓存)多级缓存
  • (附源码)springboot青少年公共卫生教育平台 毕业设计 643214
  • (附源码)ssm基于jsp高校选课系统 毕业设计 291627
  • (十二)python网络爬虫(理论+实战)——实战:使用BeautfulSoup解析baidu热搜新闻数据
  • (学习日记)2024.01.19
  • (一)UDP基本编程步骤
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • (转)C#调用WebService 基础
  • (转)用.Net的File控件上传文件的解决方案
  • . NET自动找可写目录
  • .NET Core 网络数据采集 -- 使用AngleSharp做html解析
  • .NET 中各种混淆(Obfuscation)的含义、原理、实际效果和不同级别的差异(使用 SmartAssembly)
  • .Net高阶异常处理第二篇~~ dump进阶之MiniDumpWriter
  • .NET构架之我见
  • .net生成的类,跨工程调用显示注释
  • .Net转前端开发-启航篇,如何定制博客园主题
  • @Autowired注解的实现原理
  • @Autowired自动装配
  • @Mapper作用
  • [ Linux 长征路第五篇 ] make/Makefile Linux项目自动化创建工具
  • [].shift.call( arguments ) 和 [].slice.call( arguments )
  • [04]Web前端进阶—JS伪数组