本文章只提供一些可以跑的代码,原理可以自己看其他博客,讲的很详细。 1.冒泡排序 冒泡排序是一种简单的排序算法,它反复遍历要排序的数列,比较每对相邻元素,如果顺序错误就交换它们。遍历数列的工作重复进行,直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。 2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 3.针对所有的元素重复以上的步骤,除了最后一个。 4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 本质上就是每次循环挑一个最大的放在后面
python:
def bubble_sort(arr):
if len(arr)==0:
return arr
for i in range(1,len(arr)):
for j in range(0,len(arr)-i):
if arr[j]>arr[j+1]:
arr[j],arr[j+1]=arr[j+1],arr[j]
return arr
c++:
template //整数或浮点数皆可使用,若要使用类(class)或结构体(struct)时必须重载大于(>)运算符
void bubble_sort(vector& arr) {
int i, j;
for (i = 0; i < arr.size() - 1; i++) {
for (j = 0; j < arr.size() - 1 - i; j++) {
if (arr[j] > arr[j + 1])
swap(arr[j], arr[j + 1]);
}
}
}
外层循环,它确保排序算法遍历数组足够的次数。这里 i
从 0 开始,(当然也可以从1开始,因为最后排序到最后两个时,只需比较1次即可,所以每个循环是n-i-1次比较,)到 arr.size() - 1
结束。每次循环都会把一个最大的元素放到它的最终位置。一般就是放在后面。下面代码都是升序排序。
内层循环,它在数组中进行实际的元素比较和交换。j
从 0 开始到 arr.size() - 1 - i
。随着外层循环的进行,每轮都会少比较一次,因为最大的元素已经被排序到了最后的位置。
冒泡排序的平均和最坏时间复杂度均为 O(n²),其中 n 是数组的长度。虽然不是特别高效,但它是一个理解和实现起来都非常简单的排序算法。
2.选择排序 选择排序是一种简单直观的排序算法,它的工作原理是每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。(每次比较交换的是冒泡,这里比较只是记录最大或最小元素的序号) 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 重复第二步,直到所有元素均排序完毕。
python:
def selection_sort(arr):
if len(arr)==0:
return arr
for i in range(len(arr)-1):
#记录最小数索引
minIndex = i
for j in range(i+1,len(arr)):
if arr[j]
C++:
template
void selection_sort(vector& arr) {
for (int i = 0; i < arr.size() - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.size(); j++) {
if (arr[j] < arr[min])
min = j;
}
swap(arr[i], arr[min]);
}
}
小结:
一般默认数组第一个元素为最小数,然后每次循环找到最小数交换后,开始下一循环。
与冒泡算法类似,小循环都是找最大最小值,不同的是,冒泡是比较一次就交换一次,而选择则是比较之后,存下最大最小值序号,再交换。
外层循环,它从数组的第一个元素开始,直到倒数第二个元素。每一次循环都会选出从当前位置到数组末尾的最小元素,并将其放到当前位置。
内层循环,它从当前位置的下一个元素开始,直到数组的末尾。内层循环的目的是找到从 i
到数组末尾的最小元素。
选择排序的时间复杂度是 O(n²),其中 n 是数组的长度。尽管这不是一个特别高效的算法,但它的实现简单,且在数据量较小的情况下表现良好。
3.插入排序 通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
python:
def insertionSort(arr):
if len(arr)==0:
return arr
for i in range(len(arr)):
preIndex = i-1
current = arr[i]
while preIndex >= 0 and arr[preIndex] > current:
arr[preIndex+1] = arr[preIndex]
preIndex-=1
arr[preIndex+1] = current
return arr
c++:
template
void insertion_sort(vector& arr) {
for (int i = 1; i < arr.size(); i++) {
//外层循环,从数组的第二个元素开始,直到最后一个元素。
//每次循环都会将当前元素插入到前面已经排好序的子数组中。
int key = arr[i];
//key 存储当前正在排序的元素。
int j = i - 1;
//j 是一个指向 key 前一个元素的指针,用于在已排序的子数组中向后扫描。
while ((j >= 0) && (key < arr[j])) {
arr[j + 1] = arr[j];
j--;
}
//内层循环,用于在已排序的子数组中找到 key 的正确位置。
//如果 key 比当前扫描的元素小,则将扫描的元素向右移动一位,
//并将 j 向前移动一位继续比较,直到找到 key 的插入位置。
arr[j + 1] = key;
//将 key 插入到找到的正确位置上。
}
}
总结,插入排序,从头循环遍历数组,当然一般是从第二个元素开始,将第一个元素当作已排序数组。如果当前元素小于已排序数组的某一个元素,则将该元素元素向后移一位,将第二个元素插入该位置。
插入排序在最好情况下的时间复杂度为 O(n),最坏情况和平均情况的时间复杂度为 O(n²),其中 n 是数组的长度。虽然对于大数据集来说不是最有效的排序算法,但在数据集较小或部分已经排序的情况下,插入排序可以非常高效。
4.希尔排序 插入排序的升级版 表中元素部分有序,然后 希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序, 待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1; 按增量序列个数 k,对序列进行 k 趟排序; 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。 间隔分组
这个希尔排序的解释可以搜b站。形象直观,这里不展开。
python:
def shell_sort(arr):
if len(arr)==0:
return arr
gap=1
while(gap0:
for i in range(gap,len(arr)):
temp=arr[i]
j = i-gap
while j>=0 and arr[j]>temp:
arr[j+gap]=arr[j]
j-=gap
arr[j+gap]=temp
gap = math.floor(gap/3)
return arr
c++:
//4.希尔排序 不稳定的算法,直接插入算法的升级版,分组内排序,然后直接排序。
template
void shell_sort1(vector& arr) {
int h = 1;
while (h < arr.size() / 3) {
h = 3 * h + 1;
}
//计算希尔排序的初始增量 h。
//增量 h 是希尔排序的关键部分,它决定了如何将原始数组分割成多个子序列。
//这里采用的是 3x + 1 增量序列。也可以是1/2*len(arr)
while (h >= 1) {
for (int i = h; i < arr.size(); i++) {
//这是外层循环,从索引 h 开始遍历数组,直到数组的末尾。
for (int j = i; j >= h && arr[j] < arr[j - h]; j -= h) {
swap(arr[j], arr[j - h]);
}
//内层循环,用于在每个子序列中进行插入排序。
//如果子序列中的元素比它 h 位置之前的元素小,就进行交换,
//直到找到正确的位置或达到子序列的开始。
}
h = h / 3;
}
}
小结:总的来说,希尔排序是直接插入排序的改进版本,它首先对数组的较大间隔部分进行排序,逐渐减小间隔直至完成整体排序。这使得希尔排序在处理大规模数据时比普通的插入排序更有效率。希尔排序的时间复杂度依赖于增量序列的选择,最坏情况下时间复杂度为 O(n^2),但对于某些增量序列,时间复杂度可以降低至接近 O(nlogn)。
5.归并排序
采用分治法:自下而上迭代,或者自上而下递归
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
重复步骤 3 直到某一指针达到序列尾;
将另一序列剩下的所有元素直接复制到合并序列尾。
思想很简单,可以用递归来做,总的来说就是时间换空间。
def merge(left,right):
result = []
while left and right:
if left[0]<=right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
while left:
result.append(left.pop(0))
while right:
result.append(right.pop(0))
return result
def mergeSort(arr):
if len(arr)==0:
return arr
if(len(arr)<2):
return arr
middle = math.floor(len(arr)/2)
left,right = arr[0:middle],arr[middle:]
return merge(mergeSort(left),mergeSort(right))
c++:
template
void merge(std::vector& array, int left, int mid, int right) {
int n1 = mid - left + 1; // 左半部分的长度
int n2 = right - mid; // 右半部分的长度
// 创建临时数组
std::vector L(n1), R(n2);
// 复制数据到临时数组
for (int i = 0; i < n1; i++)
L[i] = array[left + i];
for (int j = 0; j < n2; j++)
R[j] = array[mid + 1 + j];
// 合并临时数组回原数组
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
array[k] = L[i++];
}
else {
array[k] = R[j++];
}
k++;
}
// 复制剩余的元素
while (i < n1) {
array[k++] = L[i++];
}
while (j < n2) {
array[k++] = R[j++];
}
}
template
void merge_sort(std::vector& array, int left, int right) {
if (left < right) {
// 找到中间点
int mid = left + (right - left) / 2;
// 递归地对左右两半部分进行归并排序
merge_sort(array, left, mid);
merge_sort(array, mid + 1, right);
// 合并两半
merge(array, left, mid, right);
}
}
这个在c++中调用时left=0,right=arr.size()-1。表示数组的小标索引。这个感觉没啥好小结的,就是简单实用,并且时间复杂度为 O(n log n),其中 n 是数组的长度。归并排序的主要缺点是它需要与原始数组同样数量的额外存储空间。
6.快速排序 在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。 事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。 快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。 对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。 步骤 从数列中挑出一个元素,称为 "基准"(pivot); 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作; 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
python:
def swap(arr,i,j):
arr[i],arr[j]=arr[j],arr[i]
def partition(arr,left,right):
pivot=left
index=pivot+1
i=index
while i<=right:
if arr[i]
快排也是一种分而治之的思想,主打一个快,并且快排没有申请空间。
c++:
//快速排序
template
T partition(vector& arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j <= high - 1; j++) {
if (arr[j] < pivot) {
i++;
swap(arr[i], arr[j]);
}
}
//遍历数组的当前段,将所有小于基准值的元素移到基准值的左边。
swap(arr[i + 1], arr[high]);
//将基准值交换到其最终位置上(i + 1),然后返回这个位置的索引。
//这个索引将用于 quick_sort 函数中将数组分为两部分。
return i + 1;
}
template
void quick_sort(vector& arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quick_sort(arr, low, pi - 1);
quick_sort(arr, pi + 1, high);
}
}
partition
函数,它是快速排序中用于将数组划分为两个部分的关键函数。c++与python代码不同之处在于,将快排的基准pivot认为是左端还是右端。
小结:通过递归的方式将问题分解为较小的问题来解决。在平均和最佳情况下,快速排序的时间复杂度为 O(n log n),但在最坏情况下它会降至 O(n²)。不过,在实际应用中,快速排序通常比其他 O(n log n) 的排序算法更快,因为它的内部循环(inner loop)可以在大多数现代架构上非常高效地实现。
7.堆排序: 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。 堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列; 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列; 步骤 创建一个堆 H[0……n-1]; 把堆首(最大值)和堆尾互换; 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置; 重复步骤 2,直到堆的尺寸为 1
python:
def heapify(arr,n,i):
largest=i
left=2*i+1
right=2*i+2
if left
c++:
void heapify(vector& arr, int n, int i) {
int largest = i;
int left = 2 * i + 1; //左右子节点索引
int right = 2 * i + 2;
if (leftarr[largest])
largest = left;
//左右子节点是否存在,并且它们是否比当前节点大。如果是,更新 largest。
if (rightarr[largest])
largest = right;
if (largest != i) {
swap(arr[i], arr[largest]);
heapify(arr, n, largest);
}
//如果最大的不是当前节点,交换它们的值,并对新的子树应用 heapify 以确保其仍然是最大堆。
}
void heap_sort(vector& arr, int n) {
//从最后一个非叶子节点开始,对每个节点调用 heapify,以构建最大堆。
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
//逐个从堆中移除元素(通过将其与数组的最后一个元素交换),然后重新应用 heapify 来维持最大堆的性质。
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}
小结:堆排序也是一种很好理解的排序算法,本质是构建完全二叉树,最大堆或者做小堆两种形势。在heap_sort中有两个for循环,第一个循环构建最大堆。这里是从最后一个非叶子节点开始的即就是没在最后一层做叶子的节点,依次向上遍历构建最大堆,起始这里重复的有很多,比如只要当前顶点与左右节点比较换了之后,还要依次再比较左节点或者右节点的子节点,以达到完全最大堆,这里在构建最大堆后就实现了从顶向下。
第二个循环从堆中取值,并且让堆时刻保持最大堆。这里的的最大堆就不包含后面集合最大值了。因为这了i是从n-1开始,i--,每次循环将最大堆的父节点,也就是arr[0]与后面的a[i]交换,由于本质是二叉树,所以用递归构建是最简单的做法,也很好理解。
可以理解构建最大堆就是为了利用堆的数据结构将最大值作为父节点,然后将最大值移出来,重新构建除当前最大值之外的最大堆,查找下一个最大值,然后依次类推。
堆排序算法的时间复杂度为 O(n log n),其中 n 是数组的长度。由于堆排序在最坏情况下仍保持 O(n log n) 的时间复杂度,它在包含大量数据的数组排序中非常有效。
8.计数排序 计算排序,需要知道数据的范围,而且必须为整数,没有比较 步骤: (1)找出待排序的数组中最大和最小的元素 (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项 (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加) (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
python:
def countingSort(arr):
if len(arr)==0:
return arr
size = len(arr)
output = [0]*size
count = [0]*(max(arr)+1)
for i in range(0,size):
count[arr[i]]+=1
for i in range(1,len(count)):
count[i]+=count[i-1]
#累加计数数组。每个元素的计数包括自己的计数加上所有较小元素的计数。这个步骤是为了确定每个元素在排序数组中的位置。
i=size-1
while i>=0:
output[count[arr[i]]-1] = arr[i]
count[arr[i]]-=1
i-=1
#从后向前遍历输入数组(为了保持稳定性),根据计数数组中的计数将每个元素放在输出数组的正确位置上。每放置一个元素,就在计数数组中相应地减少计数。
for i in range(0,size):
arr[i]=output[i]
#将排序后的数据复制回原数组
return arr
c++:
//计数排序,只对整数有用
void counting_sort(vector& arr) {
int maxVal = *max_element(arr.begin(), arr.end());
vectorcount(maxVal + 1, 0);
for (int num : arr) {
count[num]++;
}
int index = 0;
for (int i = 0; i < maxVal; i++) {
while (count[i]>0){
arr[index++] = i;
count[i]--;
}
}
}
小结,这个算法也很好理解,但只对整数有用,主题也可以理解为哈希查找的一种,这里是利用数组小标来计数,还能改进就是将计数数组的大小精简到max-min+1,做一个0到max-min+1等于min-max的映射,这里c++版本并没有再用一个for循环去累计当前元素的位置。所以看起可能不太直观,不过使用一个while也就足够了。
9.桶排序 桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点: 在额外空间充足的情况下,尽量增大桶的数量 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中 同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。可以int与float都可以
这个算法说实话我没理解,看完代码就觉得很扯淡,该算法主要是将一个数据,根据最小值和最大值,将其分成几个范围段,每个范围表示一个桶,每个同理只装在这个范围里的数。然后,先是堆每个桶里的数据进行排序,用任何排序算法都行,只要它满足数据类型。然后在把排好序的桶依次拼起来就是有序得了。这里c++实现失败,很大问题是每个桶的数据类型是vector,每个桶的大小是在创建是就设定了的,如果这个桶没有往里添加数,或者添的数为0,那么最后拼一块就毫无意义。所以可能还需要改进,目前c++并没有实现,只看原理的话还是能看懂代码的。
python:
def bucketSort(arr,bucketSize=5):
"""
:param arr: 待排序数组
:param bucketSize: 每个桶的可容纳的元素数
:return: arr:排好序的数组,可以添加任意的排序算法
"""
if len(arr)==0:
return arr
minVal,maxVal=min(arr),max(arr)
bucketCount=(maxVal-minVal)//bucketSize+1
buckets=[[] for _ in range(bucketCount)]
for i in range(len(arr)):
idx=(arr[i]-minVal)//bucketSize
buckets[idx].append(arr[i])
arr.clear()
for bucket in buckets:
arr.extend(shell_sort((bucket)))
return arr
c++:
//桶排序
template
void bucketSort(vector& arr) {
int bucket_size = 5;//桶排序中每个桶可容纳的元素数
int n = arr.size();
if (n <= 1)return;
T minVal = *min_element(arr.begin(), arr.end());
T maxVal = *max_element(arr.begin(), arr.end());
int bucket_count = (maxVal - minVal) / bucket_size + 1;
vector>buckets(bucket_count, vector(bucket_size));
for (int i = 0; i < n; i++) {
int bucketIndex = static_cast((arr[i] - minVal) / bucket_size);
buckets[bucketIndex].push_back(arr[i]);
}
arr.clear();
for (int i = 0; i < bucket_count; i++) {
sort(buckets[i].begin(), buckets[i].end());
}
int idx = 0;
for (int i = 0; i < bucket_count; i++) {
if (buckets[i].empty())break;
for (T val : buckets[i]) {
//if (val) {
// arr[idx] = val;
// idx++;
//}
cout << val << endl;
}
}
}
10.基数排序 基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。 由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。 基数排序有两种方法: 这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异: 基数排序:根据键值的每位数字来分配桶; 计数排序:每个桶只存储单一键值; 桶排序:每个桶存储一定范围的数值;
python:
def countingSortForRadix(arr,exp):
"""
:param arr: 待排序数组
:param exp: 位因子
:return:
"""
n=len(arr)
output=[0]*n
count=[0]*10
for i in range(n):
index=arr[i]//exp
count[index%10]+=1
for i in range(1,10):
count[i]+=count[i-1]
i=n-1
while i>=0:
index=arr[i]//exp
output[count[index%10]-1]=arr[i]
count[index%10]-=1
i-=1
for i in range(n):
arr[i]=output[i]
def radixSort(arr):
maxVal=max(arr)
exp=1
while maxVal//exp>0:
countingSortForRadix(arr,exp)
exp*=10
return arr
c++:
//基数排序
void count_sort(vector& arr, int exp) {
int n = arr.size();
vectoroutput(n);
vectorcount(10, 0);
for (int i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
for (int i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
}
void radix_sort(vector& arr) {
int maxVal = *max_element(arr.begin(), arr.end());
for (int exp = 1; maxVal / exp > 0; exp *= 10) {
count_sort(arr, exp);
}
}
小结:最后一个基数排序,还是很好理解的,坏就坏在只对整数有作用,首先将元素按照个位排序,然后按照10位排序,一般两位数够用。
以上就是10种排序算法,一般只需要记住冒泡,选择,插入,快速,归并前5种即可。当然主要还是快速和对并,用到了递归,分治,这也是算法种常用的方法。