- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
0.1 不知道从哪搞了一张图,但是暂时只讨论几种内排序。考试要考的各种复杂度,想必会有不少强背的吧。就考前三种还记不住拖出去打死 值得一提的是,矩阵快速转置就是一种外排序
0.2 接下来介绍的可以分成四类,同样是不知道从哪搞的图。
1.选择排序
void selectSort(int arr[], int max)
{
int temp = -1, needsSwap = 0;
for (int i = 0; i < max; i++)
{
temp = i;
needsSwap = 0;
for (int j = i; j < max; j++)
{
if (arr[j] < arr[temp])
{
temp = j;
needsSwap = 1;
}
}
if(needsSwap)
Swap(&arr[i], &arr[temp]);
}
}
2.冒泡排序
这是一个极其简洁的排序思路。它持续地遍历数组,一次比较两个元素,如果它们的顺序错误就把它们交换过来,直到没有元素再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
void bubbleSort(int arr[], int max)
{
int issSwap=0;
for(int i=0;i<max;i++)
{
isSwap=0;
for(int j=0;j<max-i-1;j++)
if(arr[j]>arr[j+1])
{
swap(arr[j],arr[j+1]);
isSwap=1;
}
if(isSwap==0)
break;
}
}
3.插入排序
这是一个类似整理扑克牌的排序手段。我们从某一个元素开始,循环选择前面的元素和它比较并且决定插入点,插入前将后续元素移动以腾出空间。因此在顺序表中进行插入排序可能会导致惊人的数据移动量,带来令人不满的时间复杂度。
void insertSort(int arr[], int max)
{
for(int i=0;i<max-1;i++)
{
int current=arr[i+1];
int index=i;
while(index>=0 && arr[index]>current)
{
arr[index+1]=arr[index];
index--;
}
arr[index+1]=current;
}
}
爆破性的while循环导致了大量资源被花费在了移动数组上
4.希尔排序
历史的进程是不可阻挡的,Donald Shell 于1959年改进了插入排序算法,使其成功冲破O(n2)的诅咒。它与插入排序的不同之处在于,它会优先比较距离较远的元素。
希尔排序又叫缩小增量排序,其把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。刚才提到的移动数组的爆炸性while循环,此时被巧妙的拆分了。
先给出代码,接着再仔细分析
void shellSort(int arr[], int max)
{
int gap = max;
int i, j, k;
do
{
gap = gap / 3 + 1;
for (i = 0; i < gap; i++)
{
for (j = i + gap; j < max; j += gap)
{
if (arr[j] < arr[j - gap])
{
int temp = arr[j];
for (k = j - gap; k >= 0 && temp < arr[k]; k -= gap)
{
arr[k + gap] = arr[k];
}
arr[k + gap] = temp;
}
}
}
} while (gap > 1);
}
值得一提的是,希尔本人选取的是gap= max/2 但事实上这样会导致出现重复劳动导致效率降低。间隔序列中的数字互质通常被认为很重要:也就是说,除了1之外它们没有公约数。这个约束条件使每一趟排序更有可能保持前一趟排序已排好的效果。希尔最初以N/2为间隔的低效性就是归咎于它没有遵守这个准则。无论选择怎样的分隔,其计算过程都不应该过于复杂而拖累算法本身。不过这种算法的时间复杂度的数学解因为过于困难,还没有被求出来。
5.归并排序
警告:“想要理解递归,首先要理解递归”
归并排序采用分治法(Divide and Conquer),其是一种稳定的排序方法。通过将将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。这种做法会消耗额外的存储空间(临时数组),但能够显著降低时间复杂度。
可以发现有类似二叉树的结构,因此可以用递归实现。当然,也可以转化成迭代的形式。
用递归将数组分割,先分割左半部分再分右半(只是习惯而已),然后每个左右数组一层一层地 排序 并返回。


void merge(int arr[], int left, int mid, int right, int temp[])
{
int l = left, md = mid, mdd = mid + 1, r = right, k = 0, i = 0;
while (l <= md && mdd <= r)
{
if (arr[l] <= arr[mdd])
temp[k++] = arr[l++];
else
temp[k++] = arr[mdd++];
}
while (l <= md)
{
temp[k++] = arr[l++];
}
while (mdd <= r)
{
temp[k++] = arr[mdd++];
}
for (i = 0; i < k; i++)
arr[left + i] = temp[i];
}
void divide(int arr[], int left, int right, int temp[])
{
if (left < right)
{
int mid = (left + right) / 2;
divide(arr, left, mid, temp);
divide(arr, mid + 1, right, temp);
merge(arr, left, mid, right, temp);
}
}
void mergeSort(int arr[], int left, int right,int len)
{
int *temp=(int*)malloc(sizeof(int)*len);
divide(arr, left, right-1, temp);
free(temp);
}
想理解又理解不了很令人恼怒吧(装傻.jpg)?亲可以先去复习一下递归的概念呢(装傻.jpg)
6.快速排序
快速排序是冒泡排序的改进版,也是现有最好的一种内排序,有着最好的时间复杂度。在一些非常非常极端的情况下,它会退化成冒泡排序。它是一种不稳定的排序算法。
说白了,快速排序可以理解成为某一个指定元素寻找插入位置,这个位置的左右两个的元素要么全部大于它,要么全部小于它。随后以两个指针重叠的地方为基准,将数组分开继续调用快速排序,直到区间只有一个元素后返回,就得到了完整的排序。
void QuickSort(int* arr, int l, int r)
{
int left = l;
int right = r;
int key = arr[left];
if (left >= right) //此时排序结束了
{
return;
}
while (left < right)
{
while (left < right && key <= arr[right]) { --right; } if (key > arr[right])
{
arr[left] = arr[right];
++left;
}
while (left < right && key >= arr[left])
{
++left;
}
if (key < arr[left])
{
arr[right] = arr[left];
--right;
}
}
arr[left] = key;
QuickSort(arr, l, left - 1);
QuickSort(arr, left + 1, r);
}
但是,快速排序有一个隐患就是key的选择
比如我们对一个数组{ 9,8,7,6,5 }想通过快速排序来变成从小到大的排序。如果还是选择以第一个元素为枢纽元的话,快速排序就变成选择排序了。
所以,在实际应用中如果数据都是是随机数据,那么选择第几个做key都没有什么不妥。因为这个本来就是看“人品”的。
但是,如果是对于一些比较有规律的数据,我们的“人品”可能就不会太好的。所以常见的有两种选择策略:
一种是使用随机数来做选择。
另一种是取数组中的第一个,最后一个和中间一个,选择数值介于最大和最小之间的。
这一种又叫做“三数中值分割法”。理论上,这两种选择策略还是可能很悲剧的。但概率要小太多了。
7.堆排序
开不开心,惊不惊讶?!我居然写过这个!
其实是文字不好说明白所以才不在这里列的
随后以两个指针重叠的地方为基准,将数组分开继续调用快速排序,直到区间只有一个元素后返回,就得到了完整的排序。