[排序]选择排序、冒泡排序、插入排序、希尔排序、归并排序、快速排序、堆排序算法及比较
目录
1.选择排序
从数组中选择最小的元素,将它与数组的第一个元素交换位置,再讲数组剩下的元素中选择最小的元素,将它与数组的第二个元素交换位置,重复操作,直到将整个数组排序。
选择排序需要N2/2次比较和N次交换,对已经排序的数组也需要这么多次比较和交换操作。
public void Sort(int[] nums) {
int len=nums.length();
for(int i=0;i<len-1;i++){
int min=nums[i];
for(int j=i+1;j<len;j++){
if(nums[j]<min)
min=nums[j];
}
swap(nums[i],min);
}
}
时间复杂度:最好O(n2) 最坏O(n2)
空间复杂度:O(1)
稳定性:不稳定
2.冒泡排序
数组中相邻的元素进行比较,如果顺序就不交换,如果顺序错误就交换,每次让未排序的最小元素浮到左侧,或者最大元素移动右侧。
2.1常规版
public void Sort(int[] nums) {
int len=nums.length();
for(int i=0;i<len;i++){
//最小元素移到左侧
for(int j=len-1;j>i;j--){
if(nums[j-1]>nums[j])
swap(nums[j-1],nums[j]);
}
//如果最大元素移到右侧
/*
for(int j=0;j<len-i-1;j++){
if(nums[j]>nums[j+1])
swap(nums[j-1],nums[j]);
}
*/
}
}
时间复杂度:最好O(n2) 最坏O(n2)
空间复杂度:O(1)
稳定性:稳定
2.2第一次改进
考虑[2,1,3,4,5]进行冒泡排序
第一次排序:1,2,3,4,5
第二次排序:1,2,3,4,5
第三次排序:1,2,3,4,5
第四次排序:1,2,3,4,5
第一次循环就已经完成了排序,但是仍会继续后面的流程,显然是多余的。
为了解决这个问题,可以设置一个标志位,用来表示是否有交换,如果有交换继续下一次循环,如果没有则停止。
public void Sort(int[] nums) {
int len=nums.length();
for(int i=0;i<len;i++){
int flag=1;
//最小元素移到左侧
for(int j=len-1;j>i;j--){
if(nums[j-1]>nums[j]){
swap(nums[j-1],nums[j]);
flag=0;
}
}
if(flag==1)//如果没有交换过元素,说明已经有序
return;
}
}
这一次优化之后,假如从小到大排序[1,2,3,4,5]有序数组,则只会进入一次循环,此时的时间复杂度为O(n)。
时间复杂度:最好O(n) 最坏O(n2)
2.3第二次改进
考虑内循环长度,假如第i次排序时,最后一次产生交换的位置为index,则说明index之前的元素已经排好序了,那么第i+1次排序时,就可以直接从尾判断到index停止。
设置一个index标志位,标记最后一次产生交换时的位置,缩小内循环。
public void Sort(int[] nums) {
int len=nums.length();
int temppos=0;
int index=0;
for(int i=0;i<len;i++){
int flag=1;
//最小元素移到左侧
index=temppos;//判断到上一次排序时最后一次产生交换的位置
for(int j=len-1;j>index;j--){
if(nums[j-1]>nums[j]){
swap(nums[j-1],nums[j]);
flag=0;
temppos=j;
}
}
if(flag==1)//如果没有交换过元素,说明已经有序
return;
}
}
算法得到了进一步的优化,可以去掉内循环中多余的步骤。
由于至少需要循环进行一次比较,所以时间复杂度还是 最好O(n) 最坏O(n2)
3.插入排序
直接插入排序将无序序列中的元素插入有序序列中,遍历无序序列,拿无序序列中的元素与有序序列中的元素进行比较,找到合适的位置然后插入。
3.1常规版
public void Sort(int[] nums) {
int len=nums.length();
for(int i=0;i<len;i++){
for(int j=i+1;j>=0;j--){
if(nums[j]<num[j-1])
swap(nums[j],nmus[j-1]);
}
}
}
时间复杂度主要取决于比较次数和交换次数
比较次数1+2+3+……+n ~= n2/2
时间复杂度:最好O(n2) 最坏O(n2)
空间复杂度:O(1)
稳定性:稳定
3.2改进
考虑有序数组[1,2,3,4,5]的最后一次循环,5与前面已经排好序的[1,2,3,4]比较,5>4那么就可以停止内循环不再与前面进行比较。
设置一个flag判断第一次比较后是否产生交换,如果没有,则说明已经有序。
public void Sort(int[] nums) {
int len=nums.length();
int flag=1;
for(int i=0;i<len;i++){
for(int j=i+1;j>=0;j--){
if(nums[j]<num[j-1]){
swap(nums[j],nmus[j-1]);
flag=0;
}
if(flag)
break;
}
}
}
改进后的算法,对于有序数组只需要进行n次比较。
时间复杂度:最好O(n) 最坏O(n2)
4.希尔排序
对于数组[3,5,2,4,1],包含逆序(5,2),(5,4),(5,1),(2,1),(4,1),插入排序每次只交换相邻元素,使逆序数量减1,对于大规模的数组,排序速度很慢。希尔排序就是为了解决插入排序的局限性,通过交换不相邻的元素,每次使逆序数量减少大于1。
public void sort(int[] nums) {
int len=nums.length();
int h=len/3;
while(h>0){
for(int i=0;i<len;i++){
for(int j=i+h;j>=h;j=j-h){
if(nums[j]<num[j-h])
swap(nums[j],nmus[j-h]);
}
}
h=h/3;
}
}
这个代码不觉得似曾相识的样子吗,就是在插入排序的基础上,把每次+1相邻比较换成了每次+h个比较,然后增加了外层循环来改变h的值。因此时间复杂度与插入排序时一样的。
时间复杂度:最好O(n) 最坏O(n2)
空间复杂度:O(1)
稳定性:不稳定
5.归并排序
将数组分为两部分,分别进行排序,然后归并起来。
5.1归并方法
public void Merge(int[] nums,int start,int mid,int end){
int[] temp;
int i=start,j=mid+1,k=0;
for(int i=0;i<end;l++)//构建辅助数组
temp[i] = nums[i];
while(i<=mid&&j<=end){
if(nums[i]<=nums[j])//=保证稳定性
temp[k++] = nums[i++]
else
temp[k++] = nums[j++];
}
if(i>mid){
while(j<=end)
temp[k++] = nums[j++];
}
else{
while(i<=mid)
temp[k++] = nums[i++];
}
for(int i=0;i<end;i++)//归并结果复制回nums
nums[i] = temp[i];
}
5.2自顶向下归并排序
public void Up2DownMergeSort(int[] nums,int start,int end) {
if(start>=end)
return;
int mid = (strat + end) / 2;
Up2DownMergeSort(start,mid);
Up2DownMergeSort(mid+1,end);
Merge(nums,start,mid,end);
}
归并排序每次都将问题对半分成两个子问题,这种对半分的算法复杂度一般为 O(nlogn)。
时间复杂度:最好O(nlogn) 最坏O(nlogn)
空间复杂度:O(n)
稳定性:稳定
5.3自底向上归并排序
从单个元素开始向上成对归并。
public void Down2UpMergeSort(int[] nums) {
int len=nums.length();
int lo=2;
while(lo<=len){
for(int i=0;i<len;i=i+lo){
int j = i + lo -1;
int mid = (i + j) / 2;
Merge(nums,i,mid,j);
}
lo = lo * 2;
}
}
6.快速排序
快速排序在每一轮挑选一个基准元素,让比它大的元素移到右边,比它小的元素移到左边,一般取序列的第一个或最后一个元素作为基准。
例如[4,7,6,5,3,2,8,1],以4为基准,从右边找到第一个比4小的,从左边找到第一个比4大的,交换。
public void QuickSort(int[] nums,int start,int end) {
if(start>=end)
return;
int pos = GetPos(nums,start,end);
QuickSort(nums,start,pos-1);
QuickSort(nmus,pos+1,end);
}
public int GetPos(int[] nums,int start,int end){
int flag = nmus[start];
int left = start + 1 ;
int right = end;
while(left<right){
while(nums[left]<flag)
left++;
while(nums[right]>flag)
right--;
if(left<right)
swap(nums[left++],nums[right--]);
}
swap(nums[start],nums[right]);
return right;
}
快速排序的时间复杂度,一次划分要从两头开始搜索,直到low>=high,所以时间复杂度是O(n),整个排序算法的时间复杂度取决于划分的次数。
- 理想的情况是,每次划分所选择的中间数恰好将当前序列恰好等分,经过log2n次划分,就可得到长度为1的子表。这样整个算法的时间复杂度为O(nlog2n)。
- 最坏的情况是,每次划分所选择的中间数恰好是最大或最小数,这样长度为n的数据表的快速排序需要经过n趟划分,退化成了冒泡排序。此时整个算法的时间复杂度为O(n2)。
时间复杂度:最好O(nlogn) 最坏O(n2)
空间复杂度:O(logn)
稳定性:不稳定
6.1算法改进
1.切换到插入排序
对于很小和部分有序的数组快速排序没有插入排序效果好,而快速排序在小数组中会递归调用自己,因此,在待排序序列的长度分割到一定大小后,可以切换到插入排序。
2.随机选取基准
前面提到,如果待排序数组是有序数组,每次取序列第一个元素作为基准就退化成了冒泡排序,效率低下,为了缓解这种情况,可以每次从序列中随机选取一个元素作为基准。
3.三数取中
虽然随机选取基准减少了不好分割的机率,但如果待排序数组元素值全相等时,仍然是O(n2),为了缓解这种情况引入了三数取中。我们知道理想的情况是每次划分的中间数将当前序列等分,最佳的状态是选择序列排序后的中间值,但这很难算出来。一般的做法是选取序列头、中间、尾三个元素排列后的中间值作为基准。
4.三向切分
对于有大量重复元素的数组,可以将数组切分为三部分,小于、等于、大于,也就是说在一次切分结束后,可以把与基准相同的元素聚集在一起,下一次切分时,不在对与基准相同的元素进行切分。
例如[3,1,3,2,3,5,3,7,3]以第一个元素3为基准
第一趟快排结果为[3,1,3,2,3,5,3,7,3],切分成两个子序列[3,1,3,2]和[5,3,7,3]
三向切分第一趟快排结果为[1,2,3,3,3,3,3,7,5],切分成两个子序列[1,2]和[7,5]
对比可见,三向切分能减少迭代次数,提高效率。
public void QuickSort(int[] nums,int start,int end) {
int left = start;
int l = start+1;
int right = end;
int flag = nums[start];
while(l<=right){
if(nums[l]<flag)
swap(nums[l++],nums[left++]);//小于基准的数始终在跟基准交换,可以l++
else if(nmus[l]>flag)
swap(nmus[l],nums[right--]);//大于基准的数在跟右边的数交换,不知大小,所以不能l++
else
l++;
}
}
6.2快速选择算法
快速排序的GetPos()函数会返回一个j,使得a[0,j-1]小于a[j],a[j+1,len-1]大于a[j],因此,a[j]就是数组的第j大元素,可以利用这个函数找出数组的第j个元素。
public int select(int[] nums, int k) {
int l = 0, h = nums.length - 1;
while (h > l) {
int j = GetPos(nums, l, h);
if (j == k)
return nums[k];
else if (j > k)
h = j - 1;
else
l = j + 1;
}
return nums[k];
}
7.堆排序
7.1堆
堆中某个节点的值总是大于等于其子节点的值,并且堆是一棵完全二叉树。
堆可以用数组来表示,因为堆是一棵完全二叉树,而完全二叉树很容易用数组表示,位置k的节点的父节点在k/2位置,子节点在2k和2k+1位置。为了更清晰的描述节点的位置关系,这里不适用数组索引为0表示。
7.2上浮和下沉
在构建大顶堆时,当一个节点比父节点大时,需要交换这两个节点,交换后的节点可能仍然比父节点大,需要不断的比较和交换,把这种操作称为上浮。
private void swim(int k) {
while (k > 1 && heap(k / 2) < heap(k)) {
swap(heap(k / 2), heap(k));
k = k / 2;
}
}
类似的,在构建大顶堆时,当一个节点的值比子节点小,也需要不断向下进行比较和交换,称为下浮。如果一个节点有两个子节点,应该和两个子节点中值较大的节点进行交换。
private void sink(int k) {
while (2 * k <= N) {
int j = 2 * k;
if (j < N && heap(j) < heap(j + 1))
j++;
if (heap(k) >= heap(j))
break;
swap(heap(k) , heap(j));
k = j;
}
}
7.3插入元素
将插入元素放到数组的末尾,然后上浮到合适位置。
public void insert(int v) {
heap[++N] = v;
swim(N);
}
7.4删除最大元素
将数组顶端元素删除,将数组最后一个元素放到顶端,然后下沉到合适位置。
public int delMax() {
int max = heap[1];
swap(heap(1), heap(N--));
heap[N + 1] = null;
sink(heap(1));
return max;
}
7.5堆排序
堆排序的基本思想:将待排序序列构造成一个大顶堆,此时整个序列的最大值就是堆顶的根节点,将其与末尾元素进行交换,此时末尾为最大值,然后将剩余N-1个元素重新构造成一个大顶堆,这样会得到N的元素的第二大值,如此反复执行,便能得到一个有序序列了。
7.5.1构造堆
无序数组建立堆最直接的方式是从左到右(从上到下顺序遍历)进行上浮操作,最后构建为一个大顶堆,但是考虑当一个节点有子节点,而且有子节点的子节点,当它与它的子节点调整后,它可能仍然需要继续调整,那么继续调整之后可能会需要二次调整。
例如,第一步7,9交换,第二步7,11交换,9,11交换,之后9,10需要二次调整。
一个更高效的方式是从右到左(从下往上遍历)进行下沉操作,最后构建为一个小顶堆,如果一个节点的两个节点已经堆有序,下沉可以使以这个节点为根节点的堆有序,此时就算有二次调整也只关子节点,无关父节点。叶子节点不用下沉,从最后一个非叶子节点开始。
索引从1开始时,最后一个非叶子节点的索引为节点总数/2。
7.5.1交换堆顶元素与最后一个元素
交换之后需要进行下沉操作维持堆的有序状态。
继续交换下沉
继续交换下沉
继续交换下沉
至此,堆排序完成。
public void HeapSort(int[] nums) {
int N = nums.length;
for (int k = N / 2; k >= 1; k--)//数组从索引1开始,从最后一个非叶子节点开始构建大顶堆
sink(nums, N, k);
while (N > 1) {
swap(nums[1], nums[N--]);
sink(nums, N, 1);
}
}
private void sink(int[] nums,int N,int k) {
while (2 * k <= N) {//节点与它的父节点交换后,可能需要与子节点二次调整
int j = 2 * k;
if (j < N && nums[j] < nums[j + 1])
j++;
if (nums[k] >= nums[j])
break;
swap(nums[k] , nmus[j]);
k = j;
}
}
因为堆排序无关乎初始序列是否已经排序已经排序的状态,始终有两部分过程
- 构建初始的大顶堆的过程时间复杂度为O(n)
- 交换及重建大顶堆的过程中,需要交换n-1次,重建大顶堆的过程根据完全二叉树高度为logn向下取整的性质,[log2(n-1),log2(n-2)…1]逐步递减次交换
- 一共近似为nlogn,所以它最好和最坏的情况时间复杂度都是O(nlogn)
时间复杂度:最好O(nlogn) 最坏O(nlogn)
空间复杂度:O(1)
稳定性:不稳定
8.对比表格
算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
选择排序 | 最好O(n2) 最坏O(n2) | O(1) | 不稳定 |
冒泡排序 | 最好O(n) 最坏O(n2) | O(1) | 稳定 |
插入排序 | 最好O(n) 最坏O(n2) | O(1) | 稳定 |
希尔排序 | 最好O(n) 最坏O(n2) | O(1) | 不稳定 |
归并排序 | 最好O(nlogn) 最坏O(nlogn) | O(n) | 稳定 |
快速排序 | 最好O(nlogn) 最坏O(n2) | O(logn) | 不稳定 |
堆排序 | 最好O(nlogn) 最坏O(nlogn) | O(1) | 不稳定 |