注:该文章为经典文章,属于转载,个人对其中的格式进行了整理,并对代码进行了C++语言的实现。标记为原创,为了个人分类与查阅时方便操作。引用地址在最后。
对一序列的对象根据某个关键字进行排序。
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较,才能确定自己的位置。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均问题时间复杂度为O(n²)。
在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
比较排序的优势是:
计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素,以此来进行排序。针对数组arr,计算arr之前有多少个元素,则唯一确定了arr在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可。所有一次遍历即可解决,时间复杂度为O(n)。
非比较排序的优势是:
冒泡排序是一种简单的排序算法。它会重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序有误,就将它们交换过来。
走访数列的工作是重复地进行,知道没有在需要交换,也就是说,该数列已经排序完成。
因此,之所以称之为“冒泡排序”,是因为越小的元素会进过交换而慢慢地“浮”到数列的顶端。
void bubbleSort(vector<int> &nums) {
if(nums.size() == 0)
return;
for(int i = 0;i < nums.size();i++)
for(int j = 0;j < nums.size()-1-i;j++)
if(nums[j+1]<nums[j])
swap(nums[j], nums[j+1]);
return;
}
选择排序是表现最稳定的算法之一,因为无论什么数据,都是O(n²)的时间复杂度。因此用到这个算法时,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。
选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理时:
n个记录的的直接选择排序,可以经过n-1趟得到排序结果。算法的描述如下:
void selectionSort(vector<int> &nums) {
if(nums.size() == 0)
return;
for(int i = 0;i < nums.size();i++)
{
int minIndex = i;
for(int j = i;j < nums.size();j++)
if(nums[j] < nums[minIndex])
minIndex = j;
swap(nums[i], nums[minIndex]);
}
return;
}
插入排序(Insertion Sort)的算法描述的是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后往前扫描,找到相应位置并插入。
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
void insertionSort(vector<int> &nums) {
if(nums.size() == 0)
return;
int current = 0;
for(int i = 0;i < nums.size()-1;i++)
{
current = nums[i+1];
int preIndex = i;
while(preIndex >= 0 && current < nums[preIndex])
{
nums[preIndex + 1] = nums[preIndex];
preIndex--;
}
nums[preIndex + 1] = current;
}
return;
}
希尔排序是希尔(Donald Shell)在1959提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序进过改进后的一个更有效的版本,也称为缩小增量排序,同时也是突破O(n²)的第一批算法之一。它与插入排序的不同之处在于:
希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法进行排序;随着增量的逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件正好被分为一组,算法终止。
对于希尔排序,首先选择增量gap = length/2,缩小增量继续以gap = gap/2的方式,对于这种增量我们可以用一个序列来表示:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
void shellSort(vector<int> &nums) {
if(nums.size() == 0)
return;
int len = nums.size();
int tmp = 0;
int gap = len / 2;
while(gap > 0)
{
for(int i = gap; i < len; i++)
{
//直接插入排序
tmp = nums[i];
int preIndex = i - gap;
while(preIndex >= 0 && nums[preIndex] > tmp)
{
nums[preIndex + gap] = nums[preIndex];
preIndex -= gap;
}
nums[preIndex + gap] = tmp;
}
gap /= 2;
}
return;
}
和选择排序一样,归并排序的性能不受输入数据的影响,但是表现比选择排序好很多。代价是需要额外的内存空间。
归并排序是建立在归并操作上的一种有效有效的排序算法。这个算法是采用了分治法(Divide and Conquer) 的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列:
vector<int> merge(vector<int> left, vector<int> right)
{
vector<int> res (left.size()+right.size(), 0);
for(int index = 0, i = 0, j = 0; index < res.size();index++)
{
if(i >= left.size()) //左边的遍历完了
res[index] = right[j++];
else if(j >= right.size()) //右边的遍历完了
res[index] = left[i++];
else if(left[i] > right[j]) //左边的当前元素比右边的大
res[index] = right[j++];
else //右边的当前元素比左边的大
res[index] = left[i++];
}
return res;
}
vector<int> mergeSort(vector<int> nums)
{
if(nums.size() < 2)
return nums;
int mid = (int)nums.size()/2;
vector<int> left (nums.begin(), nums.begin()+mid);
vector<int> right (nums.begin()+mid, nums.end());
return merge(mergeSort(left), mergeSort(right));
}
快速排序的基本思想:
快速排序使用分治法将一个串分成两个子串。具体的算法描述如下:
对于快排,细说起来,其实就是给基准元素找到正确索引位置的过程。
一般来说,取第一个元素作为基准元素,然后建立一个临时变量去存储这个基准数据。然后,从数组两端扫描数组,并设置两个指标:
首先从后半部分开始:
然后开始从前往后扫描:
然后再开始从前往后遍历,直到low = high结束循环。此时,low或者high的小标就是基准数据就在数组中的正确的位置。
于是,这样一边下来,比基准数大的元素都会跑到基准的右边,比基准数小的元素跑到基准左边,基准数正好在其正确的位置。
接着,用递归的办法分别对前半部分和后半部分排序。
int partition(vector<int> &nums, int low, int high)
{
//定义基准元素
int pivotValue = nums[low];
while(low < high)
{
//当队尾的元素大于等于基准数据时,向前挪动high指针
while(low < high && nums[high] >= pivotValue)
high--;
//如果队尾元素小于基准数据时,需要将其赋值给low
nums[low] = nums[high];
//当队首元素小于等于基准数据时,向前挪动low指针
while(low < high && nums[low] <= pivotValue)
low++;
//当队首元素大于基准数据时,需要将其赋值给high
nums[high] = nums[low];
}
//跳出循环时low和high相等,此时的low或high就是基准数据的正确索引位置
//由原理部分可以很清楚的知道low位置的值并不是基准数据,所以需要将基准数据赋值给nums[low]
nums[low] = pivotValue;
return low; //返回pivot的正确位置
}
void quickSort(vector<int> &nums, int low, int high)
{
if(low < high)
{
//寻找基准数据的正确索引
int index = partition(nums, low, high);
//进行迭代,分别对前后两部分使用快排
quickSort(nums, 0, index - 1);
quickSort(nums, index + 1, high);
}
}
堆排序(Heap Sort)是指利用堆这种数据结构设计的一种排序算法。
堆积是一个近似完全的二叉树,并且同时满足堆积的性质:
//声明全局变量,用于记录数组的长度
static int len;
//调整是一个堆变成最大堆
void adjustHeap(vector<int> &nums, int i)
{
int maxIndex = i;
//如果有左子树,且左子树大于父节点,则将最大指针指向左子树
if(i*2 < len && nums[i*2] > nums[maxIndex])
maxIndex = i*2;
//如果有右子树,且右子树大于父节点,则将最大指针指向右子树
if(i*2+1 < len && nums[i*2+1] > nums[maxIndex])
maxIndex = i*2 + 1;
//如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置
if(maxIndex != i)
{
swap(nums[maxIndex], nums[i]);
adjustHeap(nums, maxIndex);
}
}
//建立最大堆
void buildMaxHeap(vector<int> &nums)
{
//从最后一个非叶子节点开始向上构建最大堆
for(int i = (len-1)/2; i >= 0;i--)
adjustHeap(nums, i);
}
//堆排序算法
void heapSort(vector<int> &nums)
{
len = nums.size();
if(len < 1)
return;
//1,构建一个大顶堆
buildMaxHeap(nums);
//2,循环对堆收尾(最大值)于末位交换,然后再重新调整最大堆
while(len > 0)
{
swap(nums[0], nums[len-1]);
len--;
adjustHeap(nums, 0);
}
return;
}
计数排序,其核心在于将输入的数据转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序是一种稳定的排序算法。计数排序使用了一个额外的数组C,其中:
然后根据数组C来将A中的元素排到正确的位置。
void countingSort(vector<int> &nums)
{
if(nums.size() == 0)
return;
int bias = 0;
int minValue = nums[0], maxValue = nums[0];
for(auto n : nums)
{
minValue = min(minValue, n);
maxValue = max(maxValue, n);
}
bias = 0 - minValue;
vector<int> bucket (maxValue - minValue + 1, 0);
//统计每个元素出现的次数
for(auto n : nums)
bucket[n + bias]++;
int index = 0, i = 0;
while(index < nums.size())
{
if(bucket[i] != 0){
nums[index] = i - bias;
bucket[i]--;
index++;
}
else
i++;
}
return;
}
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键在于映射函数的确定。
桶排序(Bucket Sort)的工作原理是:
需要注意的是,如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减少BucketSize增加下一循环桶的数量,否则会陷入死循环,导致内存溢出。
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决于各个桶之间数据进行排序的时间复杂度,因为其他部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也越少。但是空间消耗会增大。
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),其中n为数组长度,k为数组中的数的的最大的位数。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
基数排序有两种方法:
对于基数排序、计数排序、桶排序:
这三种都利用了桶的概念,但是在使用方法上有差异:
参考文章地址:
十大经典排序算法最强总结