排序算法一直都是各个公司年年都会拿出的面试题,作为初入社会寻找工作的小白,掌握各个排序算法是及其重要的。
1、冒泡排序(时间复杂度O(N^2),空间复杂度O(1),稳定)
先从简单的来,冒泡排序是学习C语言是必学的一种排序算法,它的思想也很容易理解。
给一组数据,双重循环。用一个指针指向第一个数据(且向后移动),另一个指针移动,拿第二个指针指向的数据挨个与第一个指针指向的数据比较,将大的数据向后交换。
void BubbleSort(int *a, int n)
{
assert(a);
for (int i = 0; i < n; i++)
{
for (int j = i; j < n; j++)
{
if (a[i] > a[j])
swap(a[i], a[j]);
}
}
}
2、选择排序(时间复杂度O(N^2),空间复杂度(O(1)),不稳定)
双重循环,内循环负责找出最小的那个数据,找到之后与外循环指向的数据进行交换。
void SelectSort(int *a, int n)
{
assert(a);
for (int i = 0; i < n - 1; i++)
{
int minIndex = i;
for (int j = i + 1; j < n; j++)
{
if (a[j] < a[minIndex])
minIndex = j;
}
if (minIndex != i)
swap(a[i], a[minIndex]);
}
}
同时找出最大与最小的数据,分别与数组两头数据交换。
void SelectSort(int *a, int n)
{
assert(a);
int i = 0;
int j = n - 1;
for (; i <= j; i++, j--)
{
int begin = i ;
int end = j - 1;
int minIndex = i;
int maxIndex = j;
while (begin <= j || end >= i)
{
if (a[begin] < a[minIndex])
minIndex = begin;
if (a[end] > a[maxIndex])
maxIndex = end;
begin++;
end--;
}
swap(a[i], a[minIndex]);
swap(a[j], a[maxIndex]);
}
}
3、直接插入排序(时间复杂度O(N^2),空间复杂度(O(1)),稳定)
双重循环。数据被分为两部分,始终保持前面部分有序。
举一个栗子:
2 2 4 9 3 6 34 7 2 5
2 2 4 9还是有序的,当end指向9,tmp指向3时。此时需要将3向前移动,挨个与9 4 2比较。我们需要将3插入2与4之间,依次将9 4向后移动一位,因为3已结保存在tmp中,不用担心被覆盖。最后将3放在原本4的位置就好了。
void InsertSort(int *a, int n)
{
assert(a);
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[i + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
break;
}
a[end + 1] = tmp;
}
}
4、希尔排序(时间复杂度O(N)~O(N^2),空间复杂度(O(1)),不稳定)
与直接插入排序原理一致。定义一个gap,直接插入排序相当于gap等于1的情况,即每次比较的范围为gap,gap逐渐缩小,直到变为1。
void ShellSort(int *a, int n)
{
assert(a);
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[i + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
5、堆排序(时间复杂度O(NlogN),空间复杂度(O(1)),不稳定)
将存放数据的数组看做一个完全二叉树的结构。将大的数据向下调整,并逐渐缩小调整范围。
void _AdjustDown(int *a, int i, int n)
{
assert(a);
int parent = i;
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
swap(a[child], a[parent]);
parent = child;
child = child* 2 + 1;
}
}
void HeapSort(int *a, int n)
{
assert(a);
for (int i = n-1; i >= 0; i--)
{
_AdjustDown(a, i, n);
}
for (int i = n-1; i > 0; --i)
{
swap(a[0], a[i]);
_AdjustDown(a, 0, i);
}
}
6、快速排序(时间复杂度O(NlogN),空间复杂度(O(logN)),不稳定)
快速排序这里介绍三种方法:左右指针法、前后指针法和挖坑法。
统一都用到三数取中法,取到数组中第一个数据、最后一个数据和中间的数据,比较大小,将中间的数作为key。
下面都只介绍递归一次的过程。
int GetMidIndex(int *a, int left, int right)
{
int mid = left + (right - left) >> 1;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
return left;
}
else
{
if (a[right] < a[mid])
{
return mid;
}
else if (a[left]>a[right])
{
return right;
}
else
return left;
}
}
6.1 左右指针法
递归。同时找出比key大和比key小的数,将小的数放在key的左边,大的数放在key的右边,递归缩小范围继续之前的操作。
int PartSort1(int *a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
swap(a[mid], a[right]);
int key = a[right];
int begin = left;
int end = right;
while (begin < end)
{
while (begin < end && a[begin] < key)
++begin;
while (begin<end && a[end] >key)
--end;
swap(a[begin], a[end]);
}
swap(a[begin], key);
return begin;
}
6.2 前后指针法
cur先走,直到cur指向的数据小于key,++prev如果不等于cur则交换prev与cur指向的数据,这样一来就将晓得数据换到了前面。到cur走到数组末尾之后,递归缩小范围继续交换。
int PartSort3(int *a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
swap(a[mid], a[right]);
int key = a[right];
int cur = left;
int prev = left - 1;
while (cur < right)
{
if (a[cur] < key && ++prev != cur)
{
swap(a[cur], a[prev]);
}
cur++;
}
return prev;
}
6.3 挖坑法
挖坑法就是不断找新坑的过程。先begin从左到右寻找比key大的值,找到之后begin指向的值为新坑(即key值),再end从右到左寻找比key小的值,找到之后end指向的值为新坑。最后将最初的key值赋值给begin指向的值(此时begin和end必定相等)。
int PartSort2(int *a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
swap(a[mid], a[right]);
int key = a[right];
int begin = left;
int end = right;
while (begin < end)
{
while (begin < end && a[begin] <= key)
++begin;
if (begin < end)
a[end] = a[begin];
while (begin < end && a[end] >= key)
--end;
if (begin < end)
a[begin] = a[end];
}
a[begin] = key;
return begin;
}
函数调用过程:
void QuickSort(int *a, int left, int right)
{
assert(a);
if (left >= right)
return;
int div = PartSort1(a, left, right);
QuickSort(a, left, div - 1);
QuickSort(a, div + 1, right);
}
7、归并排序(时间复杂度O(NlogN),空间复杂度(O(N)),稳定)
归并排序是建立在归并思想上的有效排序算法,主要采用的是分治法,将两个有序的序列归并为一个有序的序列的过程。
基本思想:设置begin、end、index三个指针,分别指向第一个有序序列、第二个有序序列、记录归并结果的暂存空间tmp,合并是依次比较a[begin]和a[end]的关键字,将较大(或较小)的值复制到tmp[index]中,然后将被复制关键字的指针(begin或end)加1,且index加1.重复这个过程直到两个有序序列有一个被复制完毕,此时将剩下的序列中的数据依次复制到tmp中即可。
举一个栗子:
假设我们有一个待排序列(14,12,15,13,11,16),我们将其分割为一个一个的有序序列,然后单个有序序列挨个归并为一个有序序列,具体分割、归并过程见下图:
void MergeArray(int *a, int left, int mid, int right, int *tmp)
{
int index = 0;
int begin = left;
int end = right;
int m = mid + 1;
while (begin <= mid && m <= right)
{
if (a[begin] <= a[m])
tmp[index++] = a[begin++];
else
tmp[index++] = a[m++];
}
while (begin <= mid)
tmp[index++] = a[begin++];
while (m <= right)
tmp[index++] = a[m++];
for (int i = 0; i < index; i++)
a[left + i] = tmp[i];
}
void Merge(int *a, int left, int right, int *tmp)
{
if (left >= right)
return;
int mid = (left + right) / 2;
Merge(a, left, mid, tmp);//分割
Merge(a, mid + 1, right, tmp);
MergeArray(a, left, mid, right, tmp);//归并
}
void MergeSort(int *a, int n)
{
int *p = new int[n];
Merge(a, 0, n - 1, p);
delete[] p;
}
8、计数排序(时间复杂度O(N),空间复杂度(O(最大数-最小数)),不稳定)
计数排序就是利用一个数组来记录每个数出现的次数,数组的下标表示数据。
举一个栗子:
待排序列为2 5 7 1 2 3 5 7,那么我们可以开一个大小为8的数组来存放每一个数据出现的次数。如下图:
上面的数据小而且数据与数据之间相差的也不大,我们可以直接确定要开多大的数组,但如果你不知道给出的待排序列有多少,数据有多大这种方法就不行了。所以这里我们可以做一些优化,我们可以先找出待排序列中的最大值和最小值,然后开辟最大值-最小值这么大的数组,计数的时候减去最小值之后再看应该把哪一个下标对应的值加1。
int GetMax(int *a, int n)
{
int Max = 0;
for (int i = 0; i < n; i++)
{
if (a[i]>a[Max])
a[Max] = a[i];
}
return a[Max];
}
int GetMin(int *a, int n)
{
int Min = 0;
for (int i = 0; i < n; i++)
{
if (a[i]<a[Min])
a[Min] = a[i];
}
return a[Min];
}
void CountSort(int *a, int n)
{
int Max = GetMax(a, n);
int Min = GetMin(a, n);
int len = Max - Min + 1;
int *count = new int[len];
for (int i = 0; i < len; i++)
count[i] = 0;
for (int i = 0; i < n; i++)
{
count[a[i] - Min]++;
}
for (int i = 0; i < len; i++)
{
while (count[i])
{
if (count[i] > 0)
cout << i + Min << " ";
count[i]--;
}
}
}
9、基数排序(时间复杂度O(N*位数),空间复杂度O(N),稳定)
按照平日里我们比较两个数的大小,我们一定是从高位开始比较,高位相同则比较次高位,直到比较出大小。那么排序算法里面我们也可以采用这种思想来实现排序。
基数排序还分为LSD和MSD两种,LSD指从数据的低位开始比较,MSD则是从数据的高位开始比较。当数据的位数比较少时,我们可以采用LSD,位数较多时MSD的效率会更高。
基数排序就是“分配”和“收集”的过程。
假设一组数据:
73 22 93 43 55 14 28 65 39 81
那么首先根据个位数的数值的大小,将这10个数据分配到对应的数组桶中,如下图
分配结束后,根据数组桶中的顺序将数据收集出来,得到半有序的数据(个位有序)
81 22 73 93 43 14 55 65 28 39
同理再对十位上的数值进行以上相同的操作,如下图
则最后收集的数据序列就是有序的序列了
14 22 28 39 43 55 65 73 81 93
int GetDigit(int *a, int n)//获得位数
{
int digit = 1;
int base = 10;
for (int i = 0; i < n; i++)
{
while (a[i] >= base)
{
++digit;
base *= 10;
}
}
return digit;
}
void RadixSort(int *a, int n)
{
int digit = GetDigit(a, n);
int *bucket = new int[n];
int base = 1;
for (int j = 0; j < digit; j++)
{
int *counts = new int[10];
for (int i = 0; i < 10; i++)
counts[i] = 0;
for (int i = 0; i < 10; i++)
{
int m = (a[i] / base) % 10;
counts[m]++;
}
int *starts = new int[10];
for (int i = 0; i < 10; i++)
starts[i] = 0;
for (int i = 1; i < 10; i++)
{
starts[i] = starts[i - 1] + counts[i - 1];
}
for (int i = 0; i < n; i++)
{
int m = (a[i] / base) % 10;
bucket[starts[m]++] = a[i];
}
memcpy(a, bucket, sizeof(int)*n);
base *= 10;
delete[] counts;
delete[] starts;
}
delete[] bucket;
}