零、关于排序稳定性的一些总结:
0.1么是稳定性?
答:排序算法的稳定性就是说保证排序前的2个相同数字的前后位置在排序后不发生改变。
0.2几种算法稳定性比较:
稳定性排序:冒泡、插入、归并、基数
不稳定排序:希尔、选择、堆排序、快速排序
0.3稳定性分析:
0.1冒泡:比较相邻位置的元素,比较大小然后选择是否交换,如果相等不会做处理,所以如果两个数相等,排序后他们的前后位置是不变的。所以冒泡排序是稳定的,这个比较好理解。
0.2选择排序:选择排序就是第n趟选择第n大/小的数据,放到第n的位置(第一趟选择最小的放在1位,第二趟选择第二小的放在2位······)。那么在换来换去的过程中很可能将两个相同数字的顺序打乱。比如:5(a号) 8 5(b号) 2 9,
第一趟选择后:2 8 5(b号) 5(a号) 9 你看,两个相同的5的前后顺序就变了。所以,选择排序不稳定。
0.3插入排序:在一个已经有序的小序列的基础上,一次插入一个数据。第一趟,小序列只有一个数字(第一个元素)。在插入时的比较也是比较大小,只有大于/小于在查到当前元素的前面,若相等,不会操作,所以插入排序不会改变两个相等元素的前后位置,所以是稳定的。
0.4快速排序:快排的思想就是把当前序列的第一个元素放到该元素最终排好序的指定位置,然后在递归实现每个分隔后的小序列。这样分隔成小序列后很可能把之前的稳定性打乱。所以快速排序是不稳定的。
0.5归并排序:把序列递归分成短序列,然后合并的过程中排好序。短序列合并时我们可以保证两个相等元素不发生顺序变换,所以归并排序是稳定的,
0.6希尔排序:是多个插入排序,一次插入排序是稳定的。但是希尔排序中,不同的插入排序时,相同的元素可能在各自的插入排序中移动,最后的排序稳定性就会打乱。所以shell排序是不稳定的。
0.7堆排序:堆的结构是结点i的孩子为2*i和2*i+1结点,大根堆要求父节点大于等于孩子结点,小根堆相反。
例如:一个长为n的序列,堆排序的过程是从第n/2开始,n/2这个结点为parent结点,parent结点和两个孩子结点比较时,顺序不会打乱。但是,n/2 -1 ,n/2 -2的这些parent结点选择元素时,就会破坏稳定性。所以堆排序不稳定。
一、冒泡:每次选择两个元素,按照需求进行交换(比如需要升序排列的话,把较大的元素放在靠后一些的位置),循环 n 次(n 为总元素个数),这样小的元素会不断 “冒泡” 到前面来,时间复杂度O(n^2)
void BubbleSort(int *arr,int len)
{
int tmp = 0;
bool flag = true;
for(int i = 0;i<len && flag;i++)
{
flag = false;
for(int j = 0;j<len-i-1;j++)
{
if(arr[j]>arr[j+1])
{
flag = true;
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
二、插入排序:每次选择一个元素,并且将这个元素和整个数组中的所有元素进行比较,然后插入到合适的位置,图片演示如上,时间复杂度 O(n^2)
代码如下:
void InsertSort(int *arr,int len)
{
int tmp;
for(int i = 1;i<len;i++)
{
tmp = arr[i];
int j = i;
for(j;j > 0;j--)
{
if(tmp < arr[j -1])
{
arr[j] = arr[j-1];
}
else
{
break;
}
}
arr[j] = tmp;
}
}
三、希尔排序:对插入排序的改进。
static void InsertNum(int *arr, int len, int count)
{
assert(NULL != arr);
int tmp;
for (int i = count; i < len; i += count)
{
tmp = arr[i];
int j = i-count;
for (; j >= 0; j -= count)
{
if (tmp < arr[j])
{
arr[j + count] = arr[j];
}
else
{
break;
}
}
arr[j + count] = tmp;
}
}
//时间复杂度O(n^1.3 - n^1.5) 空间复杂度O(1) 不稳定
void ShellSort(int *arr, int len)
{
assert(NULL != arr);
int tmp[] = {5, 3, 1 };
for (int i = 0; i < sizeof(tmp) / sizeof(tmp[0]);i++)
{
InsertNum(arr, len, tmp[i]);
}
}
代码二:
int shellSort(int arr[], int n)
{
for (int gap = n/2; gap > 0; gap /= 2)
{
for (int i = gap; i < n; i += 1)
{
int temp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap)
arr[j] = arr[j - gap];
arr[j] = temp;
}
}
return 0;
}
四、选择排序:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。时间复杂度O(n^2),稳定。
代码如下:
void SelectSort(int *arr,int len)//bug
{
int tmp = 0;
for(int i = 0;i<len ;i++)
{
int sit = i;
for(int j = i;j<len;j++)
{
if(arr[j]<arr[sit])
{
sit = j;
}
}
tmp = arr[i];
arr[i] = arr[sit];
arr[sit]= tmp;
}
}
五、 归并排序:加入了分治法的思想,时间复杂度O(nlogn) 空间复杂度O(n)。
其算法思路如下:
1.如果给的数组只有一个元素的话,直接返回(也就是递归到最底层的一个情况)
2.把整个数组分为尽可能相等的两个部分(分)
3.对于两个被分开的两个部分进行整个归并排序(治)
4.把两个被分开且排好序的数组拼接在一起
void MergeOnly(int *arr,int len,int group)
{
assert(NULL != arr);
int i = 0;
int *tmp = (int *)malloc(len*sizeof(int));
int low1 = 0;
int high1 = low1 + group -1;
int low2 = high1 + 1;
int high2 = low2 + group -1 >=len -1 ? len-1:low2 + group -1;
while(high1 <= len -1)//这里改成high1的越界条件更易理解
{
while(low1 <= high1 && low2 <= high2)
{
if(arr[low1] <= arr[low2])
{
tmp[i++] = arr[low1++];
}
else
{
tmp[i++] = arr[low2++];
}
}
while(low1 <= high1)//没写的low1 写入
{
tmp[i++] = arr[low1++];
}
while(low2 <= high2)//没写的low2 写入tmp
{
tmp[i++] = arr[low2++];
}
low1 = high2+1;
high1 = low1 + group -1;//high可能越界 但是high越界后low2一定越界就会跳出循环 而且后续不会用到high
low2 = high1 + 1;
high2 = low2 + group -1 >=len -1 ? len-1:low2 + group -1;
}
while(low1 <= len -1)
{
tmp[i++] = arr[low1++];
}
for(int j = 0;j < len;j++)
{
arr[j] = tmp[j];
}
free(tmp);
}
void MergeSort(int *arr,int len)//归并 时间复杂度O(logn)
{
assert(NULL != arr);
for(int i = 1;i<len;i*=2)
{
MergeOnly(arr,len,i);
}
}
六、快速排序:间复杂度并不固定,如果在最坏情况下(元素刚好是反向的)速度比较慢,达到 O(n^2)(和选择排序一个效率),但是如果在比较理想的情况下时间复杂度 O(nlogn)。
快排也是一个分治的算法,快排算法每次选择一个元素并且将整个数组以那个元素分为两部分,根据实现算法的不同,元素的选择一般有如下几种:
- 永远选择第一个元素
- 永远选择最后一个元素
- 随机选择元素
- 取中间值
整个快速排序的核心是分区(partition),分区的目的是传入一个数组和选定的一个元素,把所有小于那个元素的其他元素放在左边,大于的放在右边。
代码如下:
//快速排序
int Potation(int *arr,int low,int high)//找到low下标对应的数据在排序后的位置
{
assert(NULL != arr);
int tmp = arr[low];
while(low < high)
{
while(low < high && arr[high] >= tmp)//先从后面找小于tmp的
{
high--;
}
if(low <high)
{
arr[low++] = arr[high];
}
while(low < high && arr[low] <= tmp)//从前面找大于tmp的
{
low++;
}
if(low < high)
{
arr[high--] = arr[low];
}
}
arr[low] = tmp;
return low;
}
void FastSort(int *arr,int low,int high)//一:递归做法
{
assert(NULL != arr);
if(low == high)
{
return ;
}
int sit = Potation(arr,low,high);
if(low+1 < sit)
{
FastSort(arr,low,sit-1);
}
if(high-1 >sit)
{
FastSort(arr,sit+1,high);
}
}
void FastStack(int *arr,int low,int high)//递归转入栈出栈的操作
{
assert(NULL != arr);
stack<int> sort;
sort.push(low);
sort.push(high);
while(!sort.empty())
{
int high = sort.top();
sort.pop();
int low = sort.top();
sort.pop();
int sit = Potation(arr,low,high);
if(low+1 < sit)
{
sort.push(low);
sort.push(sit-1);
}
if(high-1 >sit)
{
sort.push(sit+1);
sort.push(high);
}
}
}
void QuickSort(int *arr,int len)
{
FastStack(arr,0,len-1);
}
七、堆排序:堆排序是一种基于二叉堆(Binary Heap)结构的排序算法,所谓二叉堆,我们通过完全二叉树来对比,只不过相比较完全二叉树而言,二叉堆的所有父节点的值都大于(或者小于)它的孩子节点,像这样:
首先需要引入最大堆的定义:
- 最大堆中的最大元素值出现在根结点(堆顶)
- 堆中每个父节点的元素值都大于等于其孩子结点
//堆排序
void sortonly(int *arr,int parent,int end)//做parent的运算 end是最后一个结尾的下标
{
assert(NULL != arr);
for(int i = parent*2+1;i<=end;i=2*i+1)
{
if(i+1 <= end && arr[i] < arr[i+1])
{
i++;
}
if(arr[i] > arr[parent])
{
int tmp = arr[i];
arr[i] = arr[parent];
arr[parent] = tmp;
parent = i;
}
else
{
break;
}
}
}
void HeapSort(int *arr,int len)//找到每一个parent 然后调用sortonly
{
assert(NULL != arr);
for(int i = (len-1-1)/2;i >= 0;i--)//i是每一个parent
{
sortonly(arr,i,len-1);//处理每一个parent结点
}
for(int i = 0;i<len-1;i++)//10个数的话 就只需要循环8次
{
int tmp = arr[0];
arr[0] = arr[len-1-i];
arr[len-1-i] = tmp;
sortonly(arr,0,len-1-1-i);//第一次的end 最后的下标为len-1(最后一个元素)再减1,因为最后一个已经交换过,是最大的数字
}
}
画了一个堆排序的演示图: