排序的稳定性, 稳定就是相等的项, 不会改变原来的位置顺序.
冒泡排序
稳定, 时间复杂度O(n^2).
由图可知, 冒泡排序是双层循环, 外层循环的效果即为每一层最后一个就会出现一个排序完成的元素; 内层循环的效果即为每相邻两个元素都进行比较大小, 然后大的放后面小的在前面.
外层循环次数: n-1次
内层循环次数: n-i-1次
//实现代码
//交换两个数字
void swapNum(int* num1, int* num2)
{
*num1 = *num1 ^ *num2;
*num2 = *num1 ^ *num2;
*num1 = *num1 ^ *num2;
}
//冒泡排序
void bubbleSort(int source[], int sz)
{
if (source == NULL)
{
return;
}
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - i - 1; j++)
{
if (source[j] > source[j + 1]) //从小到大顺序
{
swapNum(&source[j], &source[j + 1]);
count++;
}
}
}
}
插入(直插)排序
稳定, 时间复杂度O(n^2)
外层循环的逻辑即为,排列好前n个元素, 内层循环即为依次判断进行向后移动, 特点是前面的元素是有序的, 所以内层条件判断为只要待插元素小于前面的元素就执行移动.
外层循环次数: i从1开始, n-1次
内层循环次数: i次
//直接插入排序
void directInsertSort(int source[], int sz)
{
if (source == NULL)
{
return;
}
for (int i = 1; i < sz; i++)
{
int temp = source[i];
int j;
for (j = i; j > 0 && source[j - 1] > temp; j--) //判断前者大于后者
{
source[j] = source[j - 1];
}
source[j] = temp;
}
}
希尔排序
不稳定, 时间复杂度与采用的增量序列有关.
逻辑: 将顺序表分为多个子列, 将子列进行排序, 将原来的无序数组改为有些序的数组, 经过几次子列的排序来实现整体的排序.
步长: 每一个子列的间距, 如步长为2, 就是奇数偶数子列.
希尔排序的增量序列
- 原始的希尔排序, n/2, n/4, n/8 … 1
- Hibbard增量序列, 1, 3, 7, … , 2^k - 1. 时间复杂度为O(n^3/2)
- Sedgewick增量序列
- Knuth增量序列.
分组为逻辑上的分组, 物理上的位置还是依旧分来的, 但是顺序表的特性,以及知道步长可以很轻松找到一组的下一个元素的地址. 可以看到图上的经过了一次的移动就可以完成了排序, 使用直插和冒泡都会经过3次移动, 提高了效率.
void shellSort(int source[], int sz)
{
if (source == NULL)
{
return;
}
int n = 5, step, temp, i, j;
while (n != 0)
{
step = (1 << n) - 1; //步长
//直插
for (i = step; i < sz; i++) //从第二组开始,但是不能超过最大长度
{
temp = source[i];
for (j = i; j >= step && temp < source[j - step]; j -= step)
{
source[j] = source[j - step];
}
source[j] = temp;
}
--n;
}
}
希尔排序的最大优点,不是时间复杂度,而是数据移动次数大量减少,如果是顺序表,每一个节点都是大量的数据,减少移动次数,是很必要的,虽然增加了比较次数,但是不需要大量的数据移动操作了。
顺序存取也就是顺序表的结构,读取写入速度快,但是想要移动元素慢,因为移动一个磁盘就要转一圈。
如果是随机存取结构,链表的结构,读取写入速度慢,但是移动很快。
所以,从硬件层面来看,如果是顺序表,希尔排序快很多,如果是链表,希尔排序优势不大。希尔排序不稳定也是一个缺点。