算法中很重要的一个问题就是排序,以下总结几种常用的排序算法:(以下都按升序来算)
一、插入排序
1.思路:数组从前往后遍历,将一个小于前面有序队列的数插入前面的数中,插入之前这些数字后移。
时间复杂度:O(N^2),
稳定性:稳定
最差情况:序列较长,逆序最差
最好情况:序列较短、接近有序
代码:
void InsertSort(int *a, size_t n)
{
for (int i = 0; i<n-1; i++)
{
int end = i;
int temp = a[end + 1];//保存这个数字
while (end >= 0)//直到到数组中第一个数 停止移动
{
if (a[end] > temp)//判断是否小于前面的数
{
a[end + 1] = a[end];//向后移动
end--;
}
else//不小于就跳出循环
break;
a[end+1] = temp;
}
}
}
测试:
void TestInsertSort()
{
int a[] = { 4, 3, 7, 6, 5, 8, 9, 0, 2, 1, 12, 18, 15, 11 };
PrintArray(a, sizeof(a) / sizeof(a[1]));
InsertSort(a, sizeof(a) / sizeof(a[1]));
PrintArray(a, sizeof(a) / sizeof(a[1]));
}
二、希尔排序
希尔排序是插入排序的优化,对于较大的数组,把较小的数更快的跳到更前面的位置。
时间复杂度: O(n^1.25) ~ 1.6*O(n^1.25)
使用场景:数据较大
稳定性:不稳定
代码:(gap=3时)
void ShellSort(int *a, size_t n)
{
int gap = n;
while (gap>1)
{
gap = gap / 3 + 1;每次跳三步
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int temp = a[end + gap];
while (end >= 0)
{
if (a[end]>temp)
{
a[end + gap] = a[end];
end = end - gap;
}
else
break;
a[end+gap]=temp;
}
}
}
}
三、选择排序
原理:从第一个元素 下标为0 开始 向后遍历,找到最小的数 下标保存为min,和下标为0 处交换,完成一趟后,下次继续找,从1 开始往后找最小数和 1处的数交换,直到有序。
优化:从两边向中间找,道理一样,左边找最小的数,右边找最大的数,直到 left==right时 数组有序。
时间复杂度:O(N^2)
稳定性:不稳定
代码(优化版):
void SelectSort(int *a, size_t n)
{
assert(a);
int lhs = 0,rhs=n-1;
while (lhs < rhs)
{
int max=lhs;//每次都从lhr开始
int min = lhs;
for (int i = lhs; i <= rhs; ++i)
{
if (a[i]>=a[max])
max = i;//保存该区间最大数下标
if (a[i] <= a[min])
min = i;//保存该区间最小数
}
swap(a[min], a[lhs]);
if (max == lhs)
max = min;//判断max是否在最左边,防止交换两次
swap(a[max], a[rhs]);
++lhs;
--rhs;
}
}
四、冒泡排序
原理:冒泡排序的原理我们就不多讲了 直接上代码
时间复杂度:O(N^2)
空间复杂度:O(1)
最坏情况:逆序
最好情况:接近有序
优化:设置flag 判断每次冒泡的过程,如果这一躺未发生交换,则说明有序直接跳出
void BubbleSort(int *a, size_t n)
{
assert(a);
for (int i = 0; i < n - 1; i++)//冒泡次数为n-1次
{
bool flag = false;//设置flag
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j]>a[j + 1])//判断是否大于后面的一个数
{
swap(a[j], a[j + 1]);
flag = true;//
}
}
if (flag == false)发生交换 表示已经有序直接跳出
break;
}
}
五、堆排序
原理:用数组建大堆,然后 交换堆顶和堆尾,用end 来维护已经还未处理好的堆 。
时间复杂度:O(nlgn)
空间复杂度:O(1)
代码:
void _AdjustDown(int *a, int n, int root)//向下调整算法
{
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n&&a[child + 1] > a[child])//选出左右子树中比较小的,父节点数大于最小的就是大于子节点。
child++;
if (a[child] > a[parent])//父节点大于子节点就交换
{
swap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;//父子节点更新
}
else
break;
}
}
void HeapSort(int *a, size_t n)
{
assert(a);
for (int i = (n - 2) / 2; i >= 0; --i)
{
_AdjustDown(a, n, i);//从最后一个父节点开始向前进行向下调整算法
}
int end = n;
while (end--)
{
swap(a[0], a[end]);//每次与最后一个交换进行 向下调整算法重置堆
_AdjustDown(a, end, 0);
}
}
六、快速排序
原理:快速排序,采用递归的思想 单步思想是:每次从数组中选一个数当做key值,让比key大的数字排在key左边,让比key大的数字排在key后面,这样key的位置就是它应该在的位置
然后把key左边和右边分别作为两个数组,进行快排,就是一个递归的过程。
方法一:左右指针法
左右设置两个指针,begin,end 。begin遇到比key大的数就停止,end遇到比key小的数字就停止,这时begin和end指向的数交换,直到begin遇到end结束本次交换。
代码:
int Partition1(int *a, int left, int right)
{
int start = left;
int end = right;
int key = a[right];//定义key的值
while (start < end)
{
while (start < end&&a[start] <= key)
start++;//start指向的数比key大
while (start < end&&a[end] >= key)
end--;//end指向的数比key小
if (start < end)
swap(a[start], a[end]);//交换
}
if (start==end)//最后把a[right]和a[start]交换,注意交换a[right]而不是临时变量key
swap(a[start], a[right]);
return start;
}
方法二:填坑法
一个很形象的名字,填坑法。
原理如下:把key拿出来 此时key的位置就空出来了(实际上没有,只不过保存了key的值,可以覆盖)
设置一个指针从头到位变量
图示:
代码:
int Partition2(int *a, int left, int right)
{
assert(a&&left < right);
int key = a[right];
while (left < right)
{
while (left < right&&a[left] <= key)
left++;
a[right]=a[left];
while (left < right&&a[right] >= key)
right--;
a[left] = a[right];
}
if (left == right)
a[left]=key;
return left;
}
方法三:前后指针法
原理:设置两个指针 一前一后,两个指针先后走,prev指向比key大的就停止,cur继续向前走找到比key小的就停止,交换两个数,直到cur走到底之后,在交换 ++prev的值和cur指向的值。
int Partition3(int *a, int left, int right)
{
int cur = left;
int prev = left - 1;
int key = a[right];
while (cur < right)
{
if (a[cur] < key)
{
if (++prev != cur)
swap(a[cur], a[prev]);
}
cur++;//cur 一直走
}
if (a[cur] == key)
swap(a[cur], a[++prev]);
return prev;
}
快速排序代码:
void QuickSort(int *a ,int left , int right)
{
if (a == NULL || right <= left)
return;
int div = Partition3(a, left, right);//随意一种即可
if (div>left)
QuickSort(a, left, div-1);
if (div<right)
QuickSort(a, div+1, right);
}
快速排序的时间复杂度:O(nlgn)
最坏情况:每次拿到的都是最小值,有序情况最坏。O(N^2)
最好情况:每次拿到中间值
优化:三数取中法(数组开始、结束、中间、的三个数去一个中间值的数作为key)可以避免最坏情况产生
和小区间优化(当区间很小时,可以选择插入排序进行排序,避免递归层数很深)。
非递归版本:
非递归版本单趟排序还是和上面一致,不同的是不采用递归操作,而是拿栈存储要操作的组的左右位置,每次都把左右push进栈,再拿出来排序,直到栈为空结束。
void QuickSortNonR(int *a, int left, int right)
{
assert(a);
stack<int > s;
s.push(right);
s.push(left);
while (!s.empty())
{
int begin = s.top();
s.pop();
int end = s.top();
s.pop();
int div = Partition1(a, begin, end);
if (begin < div)
{
s.push(div - 1);
s.push(begin);
}
if (end>div)
{
s.push(end);
s.push(div + 1);
}
}
}
七、归并排序
归并排序原理:归并排序将数组分为两个部分,分别将两个子数组排成有序,再用递归的方法,最后有序
如何让两个有序数组,合并为一个有序数组:
创建一个和该数组一样大的temp空间 ,就像合并两个链表一样 从头到尾变量然后排列在新的数组上。
代码:
void merge(int *a, int *temp, int left, int mid, int right)
{
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
int index = begin1;
while (begin1 <= end1&&begin2 <= end2)
{
if (a[begin1] <= a[begin2])
temp[index++] = a[begin1++];
else
temp[index++] = a[begin2++];
}
if (begin1 > end1)//begin1走完
{
while (begin2 <= end2)
temp[index++] = a[begin2++];
}
else if (begin2 > end2)
{
while (begin1 <= end1)
temp[index++] = a[begin1++];
}
}
void _mergeSort(int *a ,int * temp, int left, int right)
{
if (left >= right)
return;
int mid = left - (left - right) / 2;
_mergeSort(a, temp, left, mid);
_mergeSort(a, temp, mid + 1, right);
merge(a, temp, left, mid, right);
memmove(a + left, temp + left, (right - left + 1)*sizeof(int));
}
void MergeSort(int * a, size_t n)
{
if (a==NULL||n <= 0)
return;
int left = 0;
int right = n - 1;
int *temp = new int[n];
_mergeSort(a,temp, left, right);
delete[] temp;
}