文章目录
- 前言
- 1. 排序算法简介
- 2 算法效率
- 2.1 度量一个程序执行时间两种方法
- 2.2 时间频度
- 2.3 时间复杂度
- 2.4 常见的时间复杂度
- 2.5 平均和最坏时间复杂度
- 3. 常见排序算法详解
- 3.1 基数排序 (Radix Sort)
- (1) 算法过程
- (2)代码实现
- 3.2 冒泡排序 (Bubble Sort)
- (1) 算法过程
- (2) 代码实现
- 3.3 快速排序 (Quick Sort)
- (1) 算法过程
- (2) 代码实现
- 3.4 插入排序 (Insertion Sort)
- (1) 算法过程
- (2) 代码实现
- 3.5 选择排序 (Selection Sort)
- (1) 算法过程
- (2) 代码实现
- 3.6 希尔排序 (Shell Sort)
- (1) 算法过程
- (2) 代码实现
- 3.7 归并排序 (Merge Sort)
- (1) 算法过程
- (2) 代码实现
- 4. 结语
前言
排序是我们在日常生活和工作中常见的一种操作。在计算机科学中,排序算法就是将一串或一组数据按照特定的顺序进行排列的算法。这些顺序可能是数字的升序或降序,也可能是字母或字词的字母顺序等。我们将探讨几种不同的排序算法,包括他们的原理、优缺点以及代码实现。
1. 排序算法简介
常见的排序算法包括:冒泡排序、快速排序、插入排序、选择排序、希尔排序、归并排序、堆排序、基数排序等。这些排序算法中,有些是比较排序,即通过比较元素之间的关系进行排序;有些是非比较排序,即不通过比较元素之间的关系进行排序,而是通过其他方法,如计数或者映射到其他空间等。
排序算法的效率通常由平均情况时间复杂度、最差情况时间复杂度、空间复杂度、稳定性等多个因素来衡量。不同的排序算法有不同的优点和缺点,适用于不同的应用场景。例如,一些排序算法适用于大数据集,而另一些则更适用于小数据集或几乎已经排好序的数据集。
此博客将详细介绍这些常见的排序算法,包括它们的工作原理,以及适用的场景等。希望通过这个博客,读者可以对这些排序算法有一个全面的理解。
2 算法效率
度量一个程序执行时间的两种主要方法是事后统计方法和事前估算方法。事后统计方法是直接运行程序,观察实际运行时间。这种方法可行,但存在两个问题:首先,为了评测算法性能,我们需要实际运行程序,这在许多情况下是不现实的;其次,实际运行时间会受到许多因素的影响,包括硬件性能、操作系统、输入数据的大小和顺序等,因此得到的结果可能并不准确。
因此,我们通常使用事前估算方法,通过分析算法的时间复杂度来预测其性能。时间复杂度是对算法执行时间增长速度的一种度量,它描述的是输入数据规模n与算法执行时间的关系。在时间复杂度的计算中,我们通常只关心最高次项,因为它在n足够大的时候,将主导整个时间复杂度。
2.1 度量一个程序执行时间两种方法
事后统计方法
这种方法可行,但是存在于两个问题:一是要想对设计的算法运行性能进行评测就需要实际去运行该程序,而是所得时间统计量依赖于计算机硬件、软件等因素,这种方式要在同一台计算机的相同状态下运行,才能比较出哪一个算法速度更快,更好。
事前估算方法
通过分析某一个算法的时间复杂度来判断哪个算法更优,更好。
2.2 时间频度
时间频度:一个算法花费的时间与算法中语句的执行次数成正比,哪一个算法中语句执行次数多,那么他所花费的时间就会多。 一个算法中语句执行次数称之为语句频度或时间频度。记为T(n)
int sum = 0;
for(int i=1;i<=n;i++){
sum+=i;
}
n = 100;
T(n) = n+1;
n*2/100
T(n) =1;
忽略常数项:
结论:
1、 2n+20 和 2n随着n变大,执行曲线无线接近,20可以忽略
2、 3n+10 和 3n随着n变大,执行曲线无限接近,10可以忽略
忽略低次项:
结论:
1、2n2+3n+10 和2n2随着n变大,执行曲线无线接近,可以忽略3n+10
2、n2+5n+20 和n2 随着n变大,执行曲线无线接近,可以忽略5n+20
忽略系数:
结论:
1、 随着n值变大,5n2+7n 和 3n2+2n执行区间重合,说明这种情况下5和3可以忽略
2、 而n3 +5n和6n3+4n执行区间分离,说明多少次方式关键
2.3 时间复杂度
在计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,渐近时间复杂度又称之为时间复杂度。
2.4 常见的时间复杂度
1、常数时间
若对于一个算法,的上界与输入大小无关,则称其具有常数时间
,记作O(1)时间
T(n) =1
2、对数时间
若算法的T(n) =O(logn),则称其具有对数时间
。
int I = 1;
while(i<n){
i=i*2;
}
x=log2n O(log2n)
3、幂对数时间
对于某个常数k,若算法的T(n) = O((logn)),则称其具有幂对数时间
4、次线性时间
对于一个算法,若其匹配T(n) = o(n),则其时间复杂度为次线性时间
(sub-linear time或sublinear time)。
5、线性时间
如果一个算法的时间复杂度为O(n),则称这个算法具有线性时间
,或O(n)时间。
for(i=1;i<=n;++i){
j=I;
j++;
}
6、线性对数时间
若一个算法时间复杂度T(n) = O(nlog n),则称这个算法具有线性对数时间
。
7、指数时间
若T(n) 是以 2为上界,其中 poly(n) 是n的多项式,则算法被称为指数时间
常见的时间复杂度对应图:
结论:
1、 常见的算法时间复杂度由小到大依次为:O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(nk)<O(2n)<Ο(n!)
2、 尽可能的避免使用指数阶的算法
2.5 平均和最坏时间复杂度
平均时间复杂度
是指所有可能的输入实例均以等概率的出现情况下得到算法的运行时间
最坏时间复杂度
,一般讨论的时间复杂度均是最坏情况下的时间复杂度,这样做的原因是最坏情况下的时间复杂度是算法在任何输入实例上运行的界限,这就保证了算法的运行时间不会比最坏情况更长。
平均时间复杂度和最坏时间复杂度
是否一样,这就需要根据算法不同而不同了。
3. 常见排序算法详解
3.1 基数排序 (Radix Sort)
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。即所有的待比较数值统一设置为同样的数位长度,位数比较短的数前面补零,然后从最地位开始依次进行一次排序,这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
(1) 算法过程
- 取得数组中的最大数,并取得位数;
- 对数组按照"指定的位数"进行稳定的排序;
- 从最低位开始,依次进行一次排序;
- 从最低位排序一直到最高位排序完成以后, 数组就变成一个有序序列。
这个过程是稳定的,也就是说,两个元素相等,它们的相对顺序不会改变。基数排序的时间复杂度为O(nk),其中n是排序元素的数量,k是数字的最大长度。
(2)代码实现
假设我们要对一个列表进行排序,我们可以使用以下java代码实现基数排序:
import java.util.*;
public class RadixSort {
public static void radixsort(int arr[], int n) {
int m = getMax(arr, n);
for (int exp = 1; m/exp > 0; exp *= 10)
countSort(arr, n, exp);
}
static int getMax(int arr[], int n) {
int mx = arr[0];
for (int i = 1; i < n; i++)
if (arr[i] > mx)
mx = arr[i];
return mx;
}
static void countSort(int arr[], int n, int exp) {
int output[] = new int[n];
int i;
int count[] = new int[10];
Arrays.fill(count,0);
for (i = 0; i < n; i++)
count[ (arr[i]/exp)%10 ]++;
for (i = 1; i < 10; i++)
count[i] += count[i - 1];
for (i = n - 1; i >= 0; i--) {
output[count[ (arr[i]/exp)%10 ] - 1] = arr[i];
count[ (arr[i]/exp)%10 ]--;
}
for (i = 0; i < n; i++)
arr[i] = output[i];
}
}
总结一下,基数排序的优点在于,对于长度为k的n个数字来说,时间复杂度可以达到线性O(nk),这对于非常大的数据量和大范围的数据来说,是很高效的一种排序算法。然而,基数排序算法实现起来相对较复杂,需要额外的存储空间,这可能是其在实际使用中较少被选择的一个原因。
3.2 冒泡排序 (Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地遍历待排序的序列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历序列的工作是重复地进行直到没有再需要交换,也就是说该序列已经排序完成。
(1) 算法过程
- 比较相邻的元素。如果第一个比第二个大(升序),就交换它们两个;
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
特点:
1、 需要循环array.length-1次 外层循环
2、 每次排序的次数逐步递减
3、 也可能存在本次排序没有发生变化
(2) 代码实现
以下是冒泡排序的一个Java实现:
public class BubbleSort {
void bubbleSort(int arr[]) {
int n = arr.length;
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-i-1; j++)
if (arr[j] > arr[j+1]) {
// swap arr[j+1] and arr[j]
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
3.3 快速排序 (Quick Sort)
快速排序是一种高效的排序算法,是对冒泡排序的一种改进,使用了分治的思想。算法选择一个元素作为“基准”,将要排序的数据分割成两部分,一部分的所有数据都比另一部分的所有数据要小。然后再按此方法对这两部分数据分别进行快速排序。它是一种被广泛使用的排序算法,性能良好的实现的期望时间复杂度为O(n log n)。
(1) 算法过程
- 从数列中挑出一个元素,称为"基准"(pivot);
- 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列进行排序。
(2) 代码实现
以下是快速排序的一个Java实现:
class QuickSort {
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = (low-1); // index of smaller element
for (int j=low; j<high; j++) {
if (arr[j] < pivot) {
i++;
// swap arr[i] and arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// swap arr[i+1] and arr[high] (or pivot)
int temp = arr[i+1];
arr[i+1] = arr[high];
arr[high] = temp;
return i+1;
}
void sort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
sort(arr, low, pi-1);
sort(arr, pi+1, high);
}
}
}
总结一下,冒泡排序和快速排序都是比较型排序,但冒泡排序是稳定排序,而快速排序是不稳定的排序。对于冒泡排序,时间复杂度为O(n²),空间复杂度为O(1);而快速排序,最好情况下时间复杂度为O(n log n),最坏情况为O(n²),平均情况为O(n log n),空间复杂度为O(log n)。
3.4 插入排序 (Insertion Sort)
插入排序是一种简单的排序算法,它模仿了人们整理扑克牌的方式。它的工作原理是通过构造一个有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序,即只需用到O(1)的额外空间的排序,最坏时间复杂度为O(n^2),使得插入排序适用于数据量小的排序。
(1) 算法过程
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
(2) 代码实现
以下是插入排序的一个Java实现:
public class InsertionSort {
void insertionSort(int arr[]) {
int n = arr.length;
for (int i = 1; i < n; ++i) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}
}
3.5 选择排序 (Selection Sort)
选择排序是一种简单直观的排序算法,无论什么数据进去都是O(n²)的时间复杂度。所以用到它的时候,数据规模越小越好。
(1) 算法过程
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
(2) 代码实现
以下是选择排序的一个Java实现:
public class SelectionSort {
void selectionSort(int arr[]) {
int n = arr.length;
for (int i = 0; i < n-1; i++) {
// Find the minimum element in unsorted array
int min_idx = i;
for (int j = i+1; j < n; j++)
if (arr[j] < arr[min_idx])
min_idx = j;
// Swap the found minimum element with the first element
int temp = arr[min_idx];
arr[min_idx] = arr[i];
arr[i] = temp;
}
}
}
总结一下,插入排序和选择排序也是比较型排序,且它们都是稳定的排序方法。对于插入排序,时间复杂度为O(n²),空间复杂度为O(1);而选择排序,时间复杂度为O(n²),空间复杂度为O(1)。尽管在最坏情况下,这两种算法都需要进行O(n²)次比较,但是插入排序在输入数据接近或已经排序的情况下,表现更优。
3.6 希尔排序 (Shell Sort)
希尔排序,也称递减增量排序,是插入排序的一种更高效的改进版本。
(1) 算法过程
希尔排序的基本思想是将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次使用一短的间隔(称为增量),然后将开始时用的最大增量减小(例如减半)。当增量减至1时,整个文件恰被分成一列,算法便终止。
(2) 代码实现
以下是希尔排序的一个Java实现:
public class ShellSort {
void shellSort(int arr[]) {
int n = arr.length;
for (int gap = n / 2; gap > 0; gap /= 2) {
for (int i = gap; i < n; i += 1) {
int temp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap)
arr[j] = arr[j - gap];
arr[j] = temp;
}
}
}
}
3.7 归并排序 (Merge Sort)
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,适用于大规模数据。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
(1) 算法过程
归并排序采用分治法(Divide and Conquer)的思想。首先将大问题分解为小问题(将待排序序列分解为尽可能相等的两部分),然后对各个小问题进行解决(对每部分分别进行排序),最后将解决小问题的答案合并起来解决原来的大问题(将两个有序的子序列合并成一个有序序列)。
我们需要将两个已经有序的子序列合并成一个有序序列,比如上图最后一次合并,将[2,4,5,6]和[1,3,7,8]已经有序的子序列合并最终序列[1,2,3,4,5,6,7,8]
(2) 代码实现
以下是归并排序的一个Java实现:
public class MergeSort {
void merge(int arr[], int left, int middle, int right) {
int n1 = middle - left + 1;
int n2 = right - middle;
int leftArray[] = new int [n1];
int rightArray[] = new int [n2];
for (int i=0; i<n1; ++i)
leftArray[i] = arr[left + i];
for (int j=0; j<n2; ++j)
rightArray[j] = arr[middle + 1+ j];
int i = 0, j = 0;
int k = left;
while (i < n1 && j < n2) {
if (leftArray[i] <= rightArray[j]) {
arr[k] = leftArray[i];
i++;
} else {
arr[k] = rightArray[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = leftArray[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArray[j];
j++;
k++;
}
}
void sort(int arr[], int left, int right) {
if (left < right) {
int middle = (left+right)/2;
sort(arr, left, middle);
sort(arr , middle+1, right);
merge(arr, left, middle, right);
}
}
}
希尔排序和归并排序都是有效的排序算法。希尔排序是一种插入排序的改进版本,其时间复杂度为O(n log n);而归并排序使用了分治策略,其时间复杂度为O(n log n),但需要O(n)的额外空间。在对效率要求较高的场景中,这两种排序方法都是不错的选择。
4. 结语
本文主要详细介绍了常见的7种排序算法:基数排序、冒泡排序、快速排序、插入排序、选择排序、希尔排序和归并排序。我们针对每一种算法都给出了算法过程的详细说明,以及对应的Java实现。每种排序算法都有其特定的应用场景,了解和掌握这些算法,可以帮助我们更好地解决实际问题。
值得注意的是,排序算法的效率会受到数据规模和数据分布的影响。因此,选择排序算法时,除了考虑其时间和空间复杂度外,还需要根据实际情况选择最适合的算法。例如,对于小规模或者部分有序的数据,插入排序可能是一个不错的选择。而对于大规模的数据,我们可能需要选择时间复杂度较低的排序算法,如快速排序、归并排序等。
这些排序算法在计算机科学和软件工程领域都有着广泛的应用,是每一个程序员必备的基础知识。希望本文的内容对你有所帮助,如果有任何问题或者建议,欢迎留言讨论。