在本文中,我们将通过动图可视化加文字的形式,循序渐进全面介绍不同类型的算法及其用途(包括原理、优缺点及使用场景)并提供 Python 和 JavaScript 两种语言的示例代码。除此之外,每个算法都会附有一些技术说明,比如使用大 \(O\) 符号来分析不同算法的时间复杂度和空间复杂度等,也提到了一些多数人都很容易理解的一些高级概述。
篇幅很长,耐心读完它,您会受益匪浅。
什么是排序算法?
数据结构中不同类型的排序算法
您需要了解的 10 大排序算法
从本质上讲,排序算法是一种计算机程序,它将数据组织成特定的顺序,例如字母顺序或数字顺序,通常是升序或降序。
排序算法主要用于以高效的方式重新排列大量数据,以便更容易地对其进行搜索和操作。它们还用于提高搜索和合并等其他算法的效率,这些算法的操作依赖于排序数据。
排序算法用于按特定顺序组织数据,这使得搜索、访问和分析更加容易。在许多应用中,排序是数据处理流程的关键部分,排序算法的效率对系统的整体性能产生重大影响。
评估一个排序算法的好坏,通常依照以下标准进行评价:
有多种类型的排序可用,排序算法的选择取决于各种因素,例如数据集的大小、要排序的数据类型以及所需的时间和空间复杂度。
比较数据集中的元素,并根据比较结果来确定两个元素中哪个应该放在序列前面。基于比较的排序算法包括:
这些不直接比较元素,而是使用数据集的其他属性来确定它们的顺序。基于非比较的排序算法包括:
这些算法对数据进行就地排序(In-place) ,这意味着他们不需要额外内存来存储中间结果,这些算法只需要少数几个指针,所以它们的空间复杂度都是 \( O(log \ n) \)。就地排序算法的示例包括:
稳定排序算法(Stable sorting)会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录 \( R \) 和 \( S \),且在原本的列表中 \( R \) 出现在 \( S \) 之前,在排序过的列表中 \( R \) 也将会是 \( S \) 之前。
这些保留了数据集中的相等元素的相对顺序。稳定排序算法的示例包括插入排序、归并排序和Timsort 等。
如果排序算法利用其输入中的现有顺序,则它属于自适应排序(Adaptive sorting)。 它受益于输入序列中的预排序,通常通过修改现有的排序算法来执行。自适应排序算法的示例包括插入排序、冒泡排序和 Timsort 等。
现在让我们来看看在排序算法中需要了解的十种常用的排序算法。
冒泡排序(Bubble sort)有时也称为“下沉排序”,是一种简单的排序算法,该排序算法是基于比较的算法。它重复遍历给定的数据列表,一次比较两个元素,如果顺序错误则交换它们,该算法一直持续到它不交换任何项为止。
冒泡排序的起源可以追溯到 20 世纪 50 年代后期,Donald Knuth 在其 1968 年的经典著作《计算机编程艺术》中对其进行了普及。
从那时起,它被广泛用于各种应用,包括编译器的排序算法、数据库中的元素排序,甚至扑克牌的排序。
冒泡排序被认为是一种相对低效的排序算法,因为它的平均复杂度和最坏情况复杂度都是 \( O(n^2) \), 这使得它的效率远低于大多数其他排序算法,例如快速排序或归并排序等。
技术说明:复杂度 \( O(n^2) \) 意味着算法完成所需的时间与输入大小的平方成正比。这意味着更大的输入会导致算法花费更长的时间才能完成。
例如,考虑一种对数字数组进行排序的算法,对一个包含 10 个数字的数组进行排序,可能需要 1 秒,但对包含 20 个数字的数组进行排序,则可能就需要 4 秒。这是因为该算法必须将数组中的每个元素与其他所有元素进行比较,因此它必须对较大的数组进行 20 次比较,而对较小的数组只需 比较 10 次。
然而,它很容易理解和实现,并且经常被用作排序的入门以及更复杂算法的构建块,但如今它在实践中很少被使用。
冒泡排序是一种简单的的算法,可用于对小型列表或元素数组进行排序。它易于实现和理解,因此可以在简单和清晰比性能更重要的情况下使用。
def bubble_sort(items):
for i in range(len(items)):
for j in range(len(items) - 1 - i):
if items[j] > items[j+i]:
items[j], items[j+1] = item[j+1], items[j]
return items
items = [10, 14, 2, 9, 14, 37, 29]
print(bubble_sort(items))
function bubbleSort(items) {
let swapped;
do {
swapped = false;
for (let i = 0; i < items.length - 1; i++) {
if(items[i] > items[i+1]) {
[items[i], items[i+1]] = [items[i+1], items[i]]
swapped = true;
}
}
} while(swapped)
return items;
}
items = [10, 14, 2, 9, 14, 37, 29]
console.log(bubbleSort(items))
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序在实现上,通常采用就地排序(即只需用到 \( O(1) \) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
在《计算机编程的艺术》中,Knuth 评论说插入排序“早在 1946 年约翰·莫奇利 (John Mauchly) 在首次发表的计算机排序讨论中就提到了”,并将其描述为一种“自然”算法,可以很容易地理解和实现。
到 1950 年代后期,Donald L. Shell 对他的 Shell 排序算法(见下文)进行了一系列改进,该方法将元素之间的距离进行比较,每次通过时距离都会减小,从而将算法的复杂度降低到 \( O(n^{3/2}) \) 和 \( O(n^{4/3}) \) 两个不同的变体中。这听起来可能不多,但对于实际应用来说这是一个相当显着的改进!
技术说明:\( O(n^{3/2}) \) 和 \( O(n^{4/3}) \) 复杂度比 \( O(n^2) \) 复杂性更有效率,这意味着它们需要更少的时间来完成。这是因为他们不需要执行与 \( O(n^2) \) 复杂度一样多的比较。
例如,使用 \( O(n^2) \) 算法对包含 10 个数字的数组进行排序可能需要 1 秒,但使用 \( O(n^{3/2}) \) 算法对同一个数组进行排序可能只需要 0.5 秒。这是因为在使用该算法时可以执行更少的比较,从而导致更快的运行时间。
2006 年,Bender、Martin Farach-Colton和 Mosteiro 发布了插入排序的一种新变体,称为库排序或“间隙插入排序(gapped insertion sort)”,它在整个数组中留下少量未使用的空间(或“间隙”),进一步改进了运行时间到 \( O(n \log n) \)。
技术说明:\( O(n \log n) \) 复杂度比 \( O(n^2) \) 以及 \( O(n^{3/2}) \) 和 \( O(n^{4/3}) \) 复杂度更有效率。这是因为它采用了分而治之的方法,这意味着它可以将问题分解成更小的部分并更快地解决它们。
例如,使用一种 \( O(n^2) \) 算法对包含 10 个数字的数组进行排序可能需要 1 秒,使用一种 \( O(n^{3/2}) \) 算法对同一个数组进行排序需要 0.5 秒,但使用一种 \( O(n \log n) \) 算法对同一个数组进行排序可能仅需要 0.1 秒。这是因为该算法可以将数组分解成更小的部分并并行求解它们,从而加快运行时间。
插入排序通常在实践中用于小数据集或作为更复杂算法的构建块。
就像冒泡排序一样,它的最坏情况和平均情况时间复杂度是 \( O(n^2) \) 。但与冒泡排序不同的是,
插入排序可用于对数据集进行就地排序,这意味着它不需要额外的内存来存储中间结果。
插入排序简单高效,常用于输入数据已经排序或输入数据规模较小的情况。它还用于对小型数据集进行排序和构建更复杂算法的块,就像冒泡排序一样。
def insertion_sort(items):
for i in range(1, len(items)):
j = i
while j > 0 and items[j-1] > items[j]:
items[j-1], items[j] = items[j], items[j-1]
j -= 1
return items
items = [6,20,8,19,56,23,87,41,49,53]
print(insertion_sort(items))
function insertionSort(items) {
for(let i = 1; i < items.length; i++) {
let j = i;
// 从后向前扫描
while(j > 0 && items[j-1] > items[j]) {
[items[j-1], items[j]] = [items[j], items[j-1]];
j--;
}
}
return items;
}
let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(insertionSort(items));
快速排序(Quicksort)是一种流行的分而治之排序算法,其基于将数组划分为两个子数组的原则——一个包含小于“枢轴”元素的元素,另一个包含大于“枢轴”元素的元素,然后递归地对这两个子数组进行排序。
快速排序的基本步骤包括:
快速排序由 Tony Hoare 于 1959 年发明,Hoare 在英国的 Elliott Brothers 计算机公司)工作时开发了该算法,作为对 Ferranti Mark I 计算机内存中的单词进行排序的一种方法。
快速排序最初于 1961 年作为研究论文发表,由于其简单、高效和易于实现,它很快成为使用最广泛的排序算法之一。
它的最坏情况时间复杂度是 \( O(n^2) \) ,当枢轴选择不当时,使其在某些情况下比其他算法(如归并排序或堆排序)效率低。
技术说明:我们不想选择太小或太大的枢轴,否则算法将以二次方时间运行。理想的情况是选择中位数作为基准,但这并不总是可行,除非我们事先了解数据分布。
快速排序作为一种高效的排序算法,有着广泛的应用。
def quick_sort(items):
if len(items) > 1:
privot = items[0]
left = [i for i in items[1:] if i < privot]
right = [i for i in items[1:] if i >= pivot]
return quick_sort(left) + [pivot] + quick_sort(right)
else:
return items
items = [6,20,8,19,56,23,87,41,49,53]
print(quick_sort(items))
function quickSort(items) {
if(items.length > 1) {
// 以第一项作为枢轴
const pivot = items[0];
let left = [], right = [];
for(let i = 1; i < items.length; i++) {
if(items[i] < pivot) {
left.push(items[i]);
} else {
right.push(items[i]);
}
}
return [...quickSort(left), pivot, ...quickSort(right)];
} else {
return items;
}
}
const items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(quickSort(items));
桶排序(Bucket sort)或称为箱排序,是一种用于对均匀分布的数据进行排序的有用算法,它可以很容易地并行化以提高性能。
其工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间 \( O(n) \)。但桶排序并不是比较排序,他不受 \( O(n \log n) \) 下限的影响。
桶排序的基本步骤包括:
桶排序的实现早在 1950 年代就已经存在,有消息称该方法自 1940 年代就已存在,它现在仍在广泛使用。
*技术说明:\( O(n+k) \) 的复杂性比 \( O(n^2) \) 、\( O(n^{3/2}) \) 和 \( O(n^{4/3}) \) 、\( O(n \log n) \) 复杂度更有效率。这是因为它只需要执行线性数量的操作,而不用考虑输入的大小。
例如,考虑一种对数字数组进行排序的算法。使用 \( O(n^2) \) 算法对一个由 10 个数字组成的数组进行排序可能需要 1 秒 ,使用 \( O(n^{3/2}) \) 算法对同一数组进行排序可能需要 0.5 秒,使用 \( O(n \log n) \) 算法对同一数组进行排序可能需要 0.1 秒,但使用 \( O(n+k) \) 算法对同一数组进行排序仅需要 0.05 秒,这是因为该算法不需要执行那么多比较。
对于非均匀分布的数据,桶排序的效率低于其他排序算法,最坏情况下的性能为 \( O(n^2) \) 。此外,它需要额外的内存来存储桶,这对于非常大的数据集来说可能是个问题。
就像快速排序一样,桶排序可以很容易地并行化并用于外排序,但是桶排序在处理均匀分布的数据时特别有用。
def bucket_sort(items):
buckets = [[] for _ in range(let(items))]
for item in items:
bucket = int(item/len(items))
buckets[bucket].append(item)
for bucket in buckets:
bucket.sort()
return [item for bucket in buckets for item in bucket]
items = [6,20,8,19,56,23,87,41,49,53]
print(bucket_sort(items))
function bucketSort(items) {
// 分成 items.length 个桶
const buckets = new Array(items.length);
for(let i = 0; i < buckets.length; i++) {
buckets[i] = []
}
// 遍历列表,根据条件往每个桶里放对应的数据
for(let j = 0; j < items.length; j++) {
let bucket = Math.floor(items[j] / items.length)
buckets[bucket].push(items[j])
}
// 对每个桶里的数据进行排序
for(let k = 0; k < buckets.length; k++) {
buckets[k].sort();
}
// 将每个桶组合到一个列表中
return [].concat(...buckets)
}
let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(bucketSort(items));
Shell 排序也称递减增量排序算法,它不是一次对整个列表进行排序,而是将列表分成更小的子列表。然后使用插入排序算法对这些子列表进行排序,从而减少排序列表所需的交换次数。Shell 排序是插入排序的一种更高效的改进版本,是非稳定排序算法。
它也被称为 "Shell 方法",其工作原理是,首先定义一个称为增量序列的整数序列,增量序列用于确定将独立排序的子列表大小,最常用的增量序列是 “Knuth 序列”,其定义如下(其中 \( h \) 是具有初始值的区间,\( n \) 是列表的长度)。
h = 1
while h < n:
h = 3*h + 1
一旦定义了增量序列,Shell 排序算法就会使用插入排序算法对子列表进行排序,以增量序列作为步长,从最大增量开始,然后向下迭代到最小增量。
当增量大小为 1
时算法停止,此时它等同于常规插入排序算法。
Shell 排序是 Donald Shell 于 1959 年发明的,作为插入排序的变体,旨在通过将原始列表分解为更小的子列表并对这些子列表进行独立排序来提高性能。
很难预测 Shell 排序的时间复杂度,因为它取决于增量序列的选择。
Shell 排序是一种通用算法,用于在各种应用程序中对数据进行排序,尤其是在对大型数据集进行排序时,例如快速排序和桶排序等。
def shell_sort(items):
sublistcount = len(items) // 2
while sublistcount > 0:
for start in range(sublistcount):
gap_insertion_sort(items, start, sublistcount)
sublistcount = sublistcount // 2
return items
def gap_insertion_sort(items, start, gap):
for i in range(start+gap, len(items), gap):
currentvalue = items[i]
position = i
while position >= gap and items[position-gap] > currentvalue:
items[position] = items[position-gap]
position = position-gap
items[position] = currentvalue
items = [6,20,8,19,56,23,87,41,49,53]
print(shell_sort(items))
function shellSort(items) {
let sublistcount = Math.floor(items.length / 2);
while(sublistcount > 0) {
for(let start = 0; start < sublistcount; start++) {
gapInsertionSort(items, start, sublistcount);
}
sublistcount = Math.floor(sublistcount / 2);
}
return items;
}
function gapInsertionSort(items, start, gap) {
for(let i = start + gap; i < items.length; i += gap) {
let currentValue = items[i];
let position = i;
while(position >= gap && items[position - gap] > currentValue) {
items[position] = items[position - gap];
position = position - gap;
}
items[position] = currentValue;
}
}
let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(shellSort(items));
归并排序(Merge sort)的基本思想是将输入列表一分为二,使用归并排序递归地对每一半进行排序,然后将排序后的两半合并在一起。合并步骤是通过重复比较每一半的第一个元素并将两者中较小的一个添加到排序列表中来执行的,重复此过程,直到所有元素都被重新合并在一起。
归并排序由 John von Neumann 于 1945 年发明,作为一种基于比较的排序算法,其工作原理是将输入列表划分为更小的子列表,递归地对这些子列表进行排序,然后将它们合并回一起以生成最终的排序列表.
归并排序在最坏情况下的时间复杂度为 \( O(n \log n) \) ,这使得它比冒泡排序、插入排序或选择排序等其他流行的排序算法更高效。
归并排序也是一种稳定排序算法,这意味着它保留了相等元素的相对顺序。
归并排序在内存使用方面有一些缺点,该算法在划分步骤中需要额外的内存来存储列表的两半,以及在合并过程中需要额外的内存来存储最终排序的列表。在对非常大的列表进行排序时,这可能是一个问题。
归并排序是一种通用排序算法,可以并行化以对大型数据集进行排序和外排序(类似快速排序和桶排序),它也常用作更复杂算法(如冒泡排序和插入排序)的构建块。
def merge_sort(items):
if len(items) <= 1:
return items
mid = len(items) // 2
left = items[:mid]
right = items[mid:]
left = merge_sort(left)
right = merge_sort(right)
return merge(left, right)
def merge(left, right):
merged = []
left_index = 0
right_index = 0
while left_index < len(left) and right_index < len(right):
if left[left_index] > right[right_index]:
merged.append(right[right_index])
right_index += 1
else:
merged.append(left[left_index])
left_index += 1
merged += left[left_index:]
merged += right[right_index:]
return merged
items = [6,20,8,19,56,23,87,41,49,53]
print(merge_sort(items))
function mergeSort(items) {
if(items.length <= 1) return items
const mid = Math.floor(items.length / 2)
const left = items.slice(0, mid)
const right = items.slice(mid)
return merge(mergeSort(left), mergeSort(right))
}
function merge(left, right) {
const merged = []
let leftIndex = 0
let rightIndex = 0
while(leftIndex < left.length && rightIndex < right.length) {
if(left[leftIndex] > right[rightIndex]) {
merged.push(right[rightIndex])
rightIndex++
} else {
merged.push(left[leftIndex])
leftIndex++
}
}
return merged.concat(left.slice(leftIndex)).concat(right.slice(rightIndex))
}
let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53]
console.log(mergeSort(items))
选择排序(Selection sort)从列表的未排序部分中,重复选择最小的元素,并将其与未排序部分的第一个元素交换,这个过程一直持续到整个列表排序完成。
红色是当前最小值,黄色是排序列表,蓝色是当前项目。
选择排序是一种简单直观的排序算法,自计算机科学早期就已存在。类似的算法很可能是由研究人员在 1950 年代独立开发的。
它是最早开发的排序算法之一,并且仍然是用于教育目的和简单排序任务的流行算法。
选择排序用于某些应用程序中,在这些应用程序中,简单性和易用性比效率更重要。它也可用作向学生介绍排序算法及其属性的教学工具,因为它易于理解和实现。
尽管选择排序很简单,但与归并排序或快速排序等其他排序算法相比,它的效率不是很高。它在最坏情况下的时间复杂度为 \( O(n^2) \),并且对大型列表进行排序可能需要很长时间。
选择排序也不是稳定的排序算法,这意味着它可能无法保留相等元素的顺序。
选择排序与冒泡排序和插入排序类似,可用于小型数据集排序,其简单性也使其成为排序算法教学和学习的有用工具。其他用途包括:
def selection_sort(items):
for i in range(len(items)):
min_idx = i
for j in range(i+1, len(items)):
if items[min_idx] > items[j]:
min_idx = j
items[i], items[min_idx] = items[min_idx], items[i]
return items
items = [6,20,8,19,56,23,87,41,49,53]
print(selection_sort(items))
function selectionSort(items) {
let minIndex;
for(let i = 0; i < items.length; i++) {
minIndex = i;
for(let j = i + 1; j < items.length; j++) {
if(items[j] < items[minIndex]) {
minIndex = j;
}
}
let temp = items[i];
items[i] = items[minIndex];
items[minIndex] = temp;
}
return items
}
let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(selectionSort(items));
基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能应用于整数。
其最坏情况下的性能为 \( {O(w\cdot n)} \),其中 \( n \) 是密钥数量,\( w \) 是密钥长度。
基数排序由 Herman Hollerith 在 19 世纪末首次引入,作为一种对穿孔卡片上的数据进行高效排序的方法,其中每一列代表数据中的一个数字。
后来,它被 20 世纪中期的几位研究人员改编并推广,用于对二进制数据进行排序,按二进制表示中的每一个比特对数据进行分组。但它也被用来对字符串数据进行排序,在排序中每个字符都被视为一个数字。
近年来,基数排序作为并行和分布式计算环境中的一种排序算法再次受到关注,因为它很容易并行化,并且可以用来以分布式方式对大数据集进行排序。
IBM 的一种卡片分类器,对一大组穿孔卡片进行基数排序。图片来源:Wikimedia Commons, Public Domain.
基数排序是一种线性时间排序算法,这意味着它的时间复杂度与输入数据的大小成正比。这使它成为对大型数据集进行排序的有效算法,尽管它可能不如其他对较小数据集的排序算法有效。
它的线性时间复杂性和稳定性使其成为对大型数据集进行排序的有用工具,它的可并行性使其对分布式计算环境中的数据排序非常有用。
基数排序也是一种稳定的排序算法,这意味着它保留了相等元素的相对顺序。
基数排序可用于需要对大型数据集进行高效排序的各种应用程序。它对字符串数据和定长键的排序特别有用,也可用于并行和分布式计算环境中。
def radix_sort(items):
max_length = False
tmp, placement = -1, 1
while not max_length:
max_length = True
buckets = [list() for _ in range(10)]
for i in items:
tmp = i // placement
buckets[tmp % 10].append(i)
if max_length and tmp > 0:
max_length = False
a = 0
for b in range(10):
buck = buckets[b]
for i in buck:
items[a] = i
a += 1
placement *= 10
return items
items = [6,20,8,19,56,23,87,41,49,53]
print(radix_sort(items))
function radixSort(items) {
let maxLength = false;
let tmp = -1;
let placement = 1;
while(!maxLength) {
maxLength = true;
let buckets = Array.from({length: 10}, () => []);
for(let i = 0; i < items.length; i++) {
tmp = Math.floor(items[i]/placement);
buckets[tmp % 10].push(items[i]);
if(maxLength && tmp > 0) {
maxLength = false;
}
}
let a = 0;
for(let b = 0; b < 10; b++) {
let buck = buckets[b]
for(let j = 0; j < buck.length; j++) {
items[a] = buck[j];
a++;
}
}
placement *= 10;
}
return items;
}
let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(radixSort(items));
梳排序(Comb sort)比较相距一定距离的元素对,如果它们乱序则交换它们。对之间的距离最初设置为正在排序的列表的大小,然后在每次通过时减少一个因子(称为“收缩因子”),直到达到最小值 \( 1 \),重复此过程,直到列表完全排序。
梳排序算法类似于冒泡排序算法,但比较元素之间的差距更大,这个更大的差距允许更大的值更快地移动到列表中的正确位置。
梳状排序算法是一种相对较新的排序算法,由 Włodzimierz Dobosiewicz 和 Artur Borowy 于 1980 年首次提出。该算法的灵感来自使用梳子理顺缠结的头发的想法,它使用类似的过程理顺未排序值的列表。
梳排序在最坏情况下的时间复杂度为 \( O(n^2) \),但在实践中,由于使用了收缩因子,它通常比其他 \( O(n ^2) \) 排序算法(如冒泡排序)更快。收缩因子使算法能够快速将大值移向正确的位置,从而减少对列表进行完全排序所需的次数。
梳排序是一种相对简单且高效的排序算法,在各种应用中有很多用例。
def comb_sort(items):
gap = len(items)
shrink = 1.3
sorted = False
while not sorted:
gap //= shrink
if gap <= 1:
sorted = True
else:
for i in range(len(items)-gap):
if items[i] > items[i+gap]:
items[i],items[i+gap] = items[i+gap],items[i]
return bubble_sort(items)
def bubble_sort(items):
for i in range(len(items)):
for j in range(len(items)-1-i):
if items[j] > items[j+1]:
items[j], items[j+1] = items[j+1], items[j]
return items
items = [6,20,8,19,56,23,87,41,49,53]
print(comb_sort(items))
function combSort(items) {
let gap = items.length;
let shrink = 1.3;
let sorted = false;
while(!sorted) {
gap = Math.floor(gap / shrink);
if(gap <= 1) {
sorted = true;
} else {
for(let i = 0; i < items.length - gap; i++) {
if(items[i] > items[i + gap]) {
[items[i], items[i+gap]] = [items[i+gap], items[i]]
}
}
}
}
return bubbleSort(items);
}
function bubbleSort(items) {
let swapped;
do {
swapped = false;
for(let i = 0; i < items.length - 1; i++) {
if(items[i] > items[i+1]) {
[items[i], items[i+1]] = [items[i+1], items[i]]
swapped = true;
}
}
} while(swapped)
return items;
}
let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(combSort(items));
Timsort 是一种混合稳定的排序算法,源自归并排序和插入排序,旨在较好地处理真实世界中各种各样的数据。
它的工作原理是将输入数据分成更小的子数组,然后使用插入排序对这些子数组进行排序,然后使用归并排序将这些已排序的子数组组合起来,生成一个完全排序的数组。
Timsort 的最坏情况时间复杂度为 \( O(n \log n) \),这使得它可以高效的对大型数据集进行排序。它也是一种稳定的排序算法,这意味着它保留了相等元素的相对顺序。
Timsort 由 Tim Peters 于 2002 年开发,用于 Python 编程语言。它是一种混合排序算法,结合了插入排序和归并排序技术,旨在有效地对各种不同类型的数据进行排序。
由于它在处理不同类型数据方面的效率和多功能性,它后来被其他几种编程语言采用,包括 Java 和 C#。
Timsort 的一个关键特性是它能够有效地处理不同类型的数据,它通过检测”运行(runs)"来做到这一点,runs
是已经排序的元素序列。然后,Timsort 以一种方式组合这些 runs
,以最大限度地减少生成完全排序数组所需的比较和交换次数。
Timsort 的另一个重要特性是它能够处理部分排序的数据。在这种情况下,Timsort 可以检测到部分排序的 runs
,并使用插入排序对其进行快速排序,从而减少对数据进行完全排序所需的时间。
作为一种高级算法,Timsort 可用于在内存受限的系统上对数据进行排序。
在编程语言排序 Timsort 由于其处理不同类型数据的效率和能力,经常被用作这些语言中的默认排序算法。
对真实世界的数据进行排序 Timsort 在对可能部分排序或包含已排序子数组的真实数据进行排序时特别有效,因为它能够检测这些 runs
并使用插入排序来快速排序,从而减少了对数据进行完全排序所需的时间。
对不同类型的数据进行排序 它旨在有效地处理不同类型的数据,包括数字、字符串和自定义对象。它可以检测相同类型的数据 runs
,并使用归并排序有效地组合它们,从而减少所需的比较和交换次数。
def insertion_sort(arr, left=0, right=None):
if right is None:
right = len(arr) - 1
for i in range(left + 1, right + 1):
key_item = arr[i]
j = i - 1
while j >= left and arr[j] > key_item:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key_item
return arr
def merge(left, right):
if not left:
return right
if not right:
return left
if left[0] < right[0]:
return [left[0]] + merge(left[1:], right)
return [right[0]] + merge(left, right[1:])
def timsort(arr):
min_run = 32
n = len(arr)
for i in range(0, n, min_run):
insertion_sort(arr, i, min((i + min_run - 1), n - 1))
size = min_run
while size < n:
for start in range(0, n, size * 2):
midpoint = start + size - 1
end = min((start + size * 2 - 1), (n-1))
merged_array = merge(
left=arr[start:midpoint + 1],
right=arr[midpoint + 1:end + 1]
)
arr[start:start + len(merged_array)] = merged_array
size *= 2
return arr
items = [6,20,8,19,56,23,87,41,49,53]
print(timsort(items))
function timsort(arr) {
const minRun = 32;
const n = arr.length;
for (let i = 0; i < n; i += minRun) {
insertionSort(arr, i, Math.min(i + minRun - 1, n - 1));
}
let size = minRun;
while (size < n) {
for (let start = 0; start < n; start += size * 2) {
const midpoint = start + size - 1;
const end = Math.min(start + size * 2 - 1, n - 1);
const merged = merge(
arr.slice(start, midpoint + 1),
arr.slice(midpoint + 1, end + 1)
);
arr.splice(start, merged.length, ...merged);
}
size *= 2;
}
return arr;
}
function insertionSort(arr, left = 0, right = arr.length - 1) {
for (let i = left + 1; i <= right; i++) {
const keyItem = arr[i];
let j = i - 1;
while (j >= left && arr[j] > keyItem) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = keyItem;
}
return arr;
}
function merge(left, right) {
let i = 0;
let j = 0;
const merged = [];
while (i < left.length && j < right.length) {
if (left[i] < right[j]) {
merged.push(left[i]);
i++;
} else {
merged.push(right[j]);
j++;
}
}
return merged.concat(left.slice(i)).concat(right.slice(j));
}
let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(timsort(items));
请注意,表中列出的时间复杂度和空间复杂度是最坏情况下的复杂度,实际性能可能因具体实现和输入数据而异。
算法 | 时间复杂度 | 空间复杂度 | 就地排序 | 稳定排序 | 自适应排序 |
---|---|---|---|---|---|
冒泡排序 | \( O(n^2) \) | \( O(1) \) | 是 | 是 | 否 |
快速排序 | \( O(n \log n) \) | \( O(\log n) \) | 是 | 否 | 是 |
桶排序 | \( O(n+k) \) | \( O(n+k) \) | 否 | 是 | 否 |
Shell 排序 | \( O(n \log n) \) | \( O(1) \) | 是 | 否 | 否 |
归并排序 | \( O(n \log n) \) | \( O(n) \) | 否 | 是 | 否 |
选择排序 | \( O(n^2) \) | \( O(1) \) | 是 | 否 | 是 |
基数排序 | \( O(w \cdot n \)) | \( O(w+n) \) | 否 | 是 | 否 |
梳排序 | \( O(n^2) \) | \( O(1) \) | 是 | 否 | 是 |
Timesort | \( O(n \log n) \) | \( O(n) \) | 是 | 是 | 是 |
最常用的排序算法可能是快速排序,它广泛用于许多编程语言,包括 C、C++、Java 和 Python,以及许多软件应用程序和库。快速排序因其在处理不同类型数据时的效率和通用性而受到青睐,并且经常被用作编程语言和软件框架中的默认排序算法。
然而,归并排序和 Timsort 等其他排序算法由于其效率和独特的特性也常用于各种应用程序。
学习算法可以提升自己的逻辑能力,真正重要的是享受「学」的过程。虽然不一定就能直接用上,但是能在编程过程中思路更加清晰,工程能力也会更好。
参考: