在文章的开始呢,这里先准备一些数据结构以及算法的可视化网站,可以让大家更直观的看到算法的运行过程:
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
首先需要申明的是,大部分的排序算法是通过交换来调整数字之间的相对顺序的,因此交换的部分是一个需要经常用到的函数,所以在介绍以下十种排序算法之间呢,我们先写一个检测交换函数:
void swap(int&a,int&b)
{
int temp = a;
a = b;
b = temp;
}
在下面的介绍中,我们将从各种排序算法的时间复杂度,空间复杂度,稳定性以及算法适用于什么样的场景来进行介绍,前面那个指标的含义我就不介绍了,稳定性是指:值相等的两个元素的相对顺序在排序前后不改变。
1最基础的冒泡排序
回到梦开始的地方,大二的那个夏天,被计算机基础的老师叫上黑板首先冒泡排序,冒泡排序采用沉底的方法,从第一个数开始,与右侧相邻的数字比较,当左右相对顺序逆序就交换,否则的话就不交换。
每一套排序之后,将会将最大的数字沉底,后面每一套比较需要对比的数字就减1.
void Bubble(int arr[] ,int L)
{
for (int i = 0; i < L; i++)
{
for (int loca = 0; loca < L - i-1; loca++)
{
if (arr[loca] > arr[loca + 1])
{
swap(arr[loca], arr[loca + 1]);
}
}
}
}
2插入排序
从字面上理解的意思,插入排序就是将没有排序的数字插入到已经排序的队列之中。
思想: 一共有i套(0<=i<n);第i套将第i个数字与前面的分别比较,遇到大的数就交换,否则就不交换,其中,我们最常见的版本就是这个:
void InsetSort2(int arr[], int L)
{
for (int i = 1; i < L; i++)
{
for (int j =i ; j >0; j--)
{
if (arr[j] < arr[j - 1])
{
swap(arr[j], arr[j - 1]);
}
}
}
}
在这种排序方式中,带排序的数字会与以排序中的每一个数字都进行比较,如果逆序就交换;我们没有办法改变交换的次数,但是却可以改变判断的次数,改变插入排序的一个好的方法的原则是:
***待排序的数字往前开始比较,当遇到下一个数字比起小,那前面的所有数字都会比其小,就没有必要再次判断。***这个概念很重要,因为插入排序的特点是对于部分有序的数组排序效率会很高,后面的希尔排序我会讲到这一点。
改进的插入排序代码如下:
void InsetSort(int arr[],int L)
{
for (int i = 0; i < L; i++)
{
int j = i;
while (j != 0 && arr[j] < arr[j - 1])
{
swap(arr[j], arr[j - 1]);
j--;
}
}
}
选择排序
规则:第一轮,将第一个数字与后面的所有数字依次比较,如果逆序就交换,如此之后,第一次数字就是最大或者最小的值。
第二轮,将第二个数字与后面的所有数字依次比较,如果逆序就交换,如果之后,第二个数字就是第二大或者第二小的值。
。。。。
void choiceSort(int arr[],int L)
{
for (int i = 0; i < L; i++)
{
for (int j = i + 1; j < L; j++)
{
if (arr[i] > arr[j])
{
swap(arr[i], arr[j]);
}
}
}
}
选择排序算法是不稳定的,为什么?这里举一个简单的列子:
对 5 8 5* 2 9 进行排序,只要一轮我们会发现结果为: 2 8 5* 5 9
5* 和 5的相对顺序就变了,这是因为比5小的第一数字2在5*的右边,发生交换之后,顺序就会改变。所以牢记这一点:选择排序是不稳定的排序!
4快速排序
思想,采用分而治之的思想,通过不断定位基准数的位置,从而再对基准数两边的位置跑排序。
首先使用分治算法的方法,一下是基本的原则:
1,左右指针分别指向数组的开始和末尾
2、选定一个基准数,这里我们设置最左边的数字为基准书
3、从右指针开始,找到小于等于基准数的数字,从左指针开始,找到大于基准数的数字(注意,如果你想要第一套排序之后等于基准数的数字都在左边,采用此方法),找到之后,交换左右指针数值,继续次过程
(PS:第三点有两点注意事项:1,等于号只能有一边,因为一套排序过程的目标是是的左边的数字都小于基准数,右边大于等于基准数,或者左边的数字小于等于基准数,右边大于基准数字,这个需要在等于号后面控制。2、一定要特别注意,远离基准数的指针先移动!!!!!)
后面循环往复。。。
代码的时候,其实快排算法的分治算法写法都是一样的讨论,一定要记清楚上面的要点。
代码:
void quickSort(int arr[],int left,int right)
{
if (left >= right)
{
return;
}
int base = left;
int end = right;
while (left<right)
{
while (arr[right] > arr[base] && left<right)
{
right--;
}
while (arr[left] <= arr[base] && left<right)
{
left++;
}
swap(arr[left], arr[right]);
cout << "交换"<< endl;
}
swap(arr[base], arr[left]);
cout << "交换" << endl;
quickSort(arr, base, left - 1);
quickSort(arr, left + 1, end);
}
这一部分留给填坑法。
希尔排序
希尔排序来源于插入排序。将大规模无序数组拆分成小规模有序数组,然后使用插入排序。希尔排序需要掌握三个要点:
1、缩小增量公式,希尔排序的时间复杂度根据不同的增量递减公式会有不同的表现,我一般常用的减小公式为 incre = incre/3 +1;(从数组长度开始),缩小增量公式一定要牢记啊!!!
2、分组
3、选择排序
void ShellDivide(int arr[], int L)
{
int incre = L;
while (incre > 1)
{
incre = incre/3 + 1;
//增量确定了
for (int i = 0; i < incre; i++)
{
//需要排序的起始位置知道了,相当于序列也就知道了
for (int base = 0; base < L; base+=incre)
{
for (int j = base; j < L; j+=incre)
{
if (arr[base] > arr[j])
{
swap(arr[base], arr[j]);
}
}
}
}
}
}
归并排序
下午花了两个小时才把归并排序从理解到代码实现的过程完成了,现在开始叙述一篇。
在了解归并排序之前,我不得不提一下在力扣官网刷到的第二个简单题,给两个有序数组合并,因为在归并排序中很好地利用了这中题目的解法。
1、归并排序采用了先分后合的思想,采用二分法将arr分割成不同层次的单元素数组为止。
2、在合并的过程中,采用了有序列表合并的思想。
3、使用递归的方法是,要注意分割和合并的顺序,采用左右分割的思想,递归的顺序采用后跟遍历的思想。先分左边,再分右边,先分的后合,这其中的机理我到现在还没有弄明白,不过可以先这样写着。
一下是代码:
void mergeSort(int arr[];int left,int right)
{
if(left<right) //只有当left <right 的时候才有必要分割
{
int mid = (left+right)/2;
mergeSort(arr,left,middle);
mergeSort(arr,middle+1,right);
merge(arr,left,middle,right);
}
}
其中 merg函数实际上就是排列有序链表的过程,也就是说arr[left,middle],
arr[middle+1,right];实际上是两个有序链表,现在将他们合并就行。
以上的核心是,在实现递归的时候要递归基是哪一个,也就是在什么情况下跳出递归。
void merge(int arr[].left ,mid,right)
{
int *p =new int[right-left+1];
int k=0;
int st =left;
int end =right;
right = mid;
while(left<=mid && right<=end)
{
if(arr[left] <=arr[right])
{
p[k++] = arr[left++];
}
else
{
p[k++] = arr[right++];
}
}
while(left<=middle)
{
p[k++] =arr[left++];
}
while(right<=end)
{
p[k++] =arr[right++];
}
//开始数组拷贝
memcpy(arr+left,p,sizeof(int)*(end-st+1));
delete [] p;
//防止内存泄露。
}
桶排序
桶排序又叫计数排序,当被排列的数字集合是有限个时(n),我们可以划分(n)个桶,然后遍历每一个元素,遍历到的时候对应元素所属的桶子计数加一,注意,桶子是有序摆放的,所以遍历玩整个数组之后,按照桶子的顺序输出即可。
时间复杂度0(n),空间复杂度根据元素集合的个数而定。比如以下两种情况:
1,
3 2 3 4 5 6 7 8 6 5 6 8 9
元素是一个有限的离散的集合,且数组元素的个数远远大于集合中元素的个数,此时空间复杂度小,采用桶排序即可
2,
3.453 4.33 5.342 7.3432 6.7645
当给浮点数进行排序的时候,因为浮点数是一个范围数,连续区间,而不是离散的空间,因此不适合桶排序
3,
1 456 1890 2987634
第三种情况,当数组元素范围远远大于数组长度的时候,需要建立的桶子的个数也远远要大于数组的长度,此时不适合桶排序,这里只有四个元素,采用简单的交换式排序方法即可。
代码
void BucketSort(int arr[],int L)
{
//首先构造一个10桶,每一个桶里面的元素都是初始化为零,而且下标表示元素序号
int bucket[10] = {0,0,0,0,0,0,0,0,0,0};
for (int i = 0; i < L; i++)
{
bucket[arr[i]]++;
}
int k = 0;
for (int i = 0; i < 10; i++)
{
//这里面的元素值就是i
for (int j = 0; j < bucket[i]; j++)
{
arr[k++] = i;
}
}
}