我对排序的基本认识
- 排序:就是将一组杂乱无章的数据按照一定的规律(升序或降序)组织起来。
- 排序码:通常数据元素有多个属性域,其中有一个属性域可用来区分元素,作为排序依据,该域即为排序码。
- 按照主排序码进行排序,排序的结果是唯一的。
- 按照次排序码进行排序,排序的结果可能是不唯一的。
排序算法稳定性
- 如果在元素序列中有两个元素R[i]和R[j],它们的排序码K[i] == k[j],且在排序之前,元素R[i]在R[j]的前面。如果在排序之后,元素R[i]仍在R[j]之前,则称这个排序算法是稳定的,否则称这个排序算法是不稳定的。
- 内部排序:数据元素全部放在内存中的排序。
- 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能 在内外存之间移动数据的排序。
常见排序算法
插入排序:
- 直接插入排序和希尔排序
直接插入排序
- 稳定性:稳定的排序
- 时间复杂度O(N^2)
- 空间复杂度O(1)
- 运用场景:
- 1、接近有序(越接近有序,效率越高)
- 2、数据量比较少
- 基本思想:用我自己的话说,第一趟,前两个数比较大小,然后从小到大排序,(我的文中都是从小到大排序,从大到小排序都是一个道理);第二趟,拿起第三个数插到已经排好序的前两个数中,使他依然有序。第三趟,拿起第四个数插到已经排好序的前三个数中,使他依然有序。。。。。。好了,后面和前面一样,继续往下做就好。
- 我举个例子
- 初始状态:{3,6,4,2,11,10,6}
- 第一趟排序:{(3,6),4,2,11,10,6}
- 第二趟排序;{(3,4,6),2,11,10,6}
- 第三趟排序:{(2,3,4,6),11,10,6}
- 第四趟排序:{(2,3,4,6,11),10,6}
- 第五趟排序:{(2,3,4,6,10,11),6}
- 第六趟排序:{(2,3,4,6,6,10,11)}
- 好了,排序完成!
- 再来看看代码
int InsertSort(int* array, int size)
{
int i, j;
assert(array);
for (i = 1; i < size; i++)
{
int Datum = array[i];
for (j = i-1; j >= 0 ; j--)
{
if (Datum < array[j])
array[j + 1] = array[j];
else
break;
}
array[j+1] = Datum;
}
}
- 我们还可以采用二分查找的方式寻找要插入的位置。
//二分查找法的插入排序
void InsertSortBS(int array[], int size)
{
int i, j, left, right, mid, key;
for (i = 1; i < size; i++)
{
key = array[i];
left = 0;
right = i - 1;
while (left <= right)
{
mid = left + ((right - left) >> 1);
if (key >= array[mid])
left = mid + 1;
else
right = mid - 1;
}
for (j = i - 1; j >= left; j--)
{
array[j + 1] = array[j];
}
array[left] = key;
}
}
希尔排序
- 又称缩小增量排序,是对直接插入排序的优化
- 数据量比较大–也不是接近有序–利用插入排序的思想来排序的话----那么我们就可以将数据量变小(分组—让他们每隔多少然后分组)—希尔排序
- 稳定性:不稳定
- 时间复杂度(与gap的取值方式有关系) N^1.25 – 1.6* N ^1.25
- 空间复杂度O(1)
- 运用场景:数据量比较大,而且比较杂乱,不是接近有序。
- gap的取值方式:可以gap /= 2 — 取奇数----取素数---- 效果最好 gap=gap/3+1(通过大量实验证明)
- 这个方法的底层还是直接插入排序,但是它将所有元素分组,直接使效率加快。
//希尔排序(分组排序)
void _InsertSort(int* array, int size, int gap)
{
int i, j, key;
for (i = 1; i < size; i++)
{
key = array[i];
for (j = i - gap; j >= 0; j -= gap)
{
if (key < array[j])
array[j + gap] = array[j];
else
break;
}
array[j + gap] = key;
}
}
void ShellSort(int* array, int size)
{
int gap = size;
while (1)
{
gap = gap / 3 + 1;
_InsertSort(array,size,gap);
if (gap == 1)
break;
}
}
- 当最后分组划分变成一组的时候,我们的排序就已经排好了。
选择排序
- 基本思想: 每一趟(第i趟,i=0,1,…,n-2)在后面n-i个待排序的数据元素集合中选出关键码最小的数据元素,作为有序元素序列的第i个元素。待到第n-2趟做完,待排序元素集合中只剩下1个元素,排序结束
【直接选择排序】
- 在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
- 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中, 重复上述步骤,直到集合剩余1个元素
- 稳定性:不稳定
- 时间复杂度O(N^2)
- 空间复杂度O(1)
- 运用场景:适合元素在数据中重复性比较高的
void SelectSort(int* array, int size)
{
int minPos = 0 , maxPos = size - 1 , minElem, maxElem;
int i = 0;
while (minPos < maxPos)
{
minElem = minPos;
maxElem = maxPos;
for (i = minPos; i <= maxPos; i++)
{
if (array[minElem] > array[i])
minElem = i;
if (array[maxElem] < array[i])
maxElem = i;
}
Swap(&array[minPos], &array[minElem]);
if (minElem == maxElem)
maxElem = minElem;
Swap(&array[maxPos], &array[maxElem]);
minPos++;
maxPos--;
}
}
- 上面代码中我们一次循环找到最大值和最小值分别和数组最后一个元素和第一个元素进行交换。然后下次排序就可以排除第一个元素和最后一个元素的干扰了。相当于我们一次循环固定两个元素的位置。
堆排序算法
- 创建堆:升序—>大堆,降序—>小堆
- 执行如下步骤,直到数组为空
- 把堆顶array[0]元素和当前最堆的最后一个元素交换
- 堆元素个数减1
- 由于第1步后根节点不再满足最堆定义,向下调整根结点
- 把一棵完全二叉树调整为堆,以及每次将堆顶元素交换后 进行调整的时间复杂度均为O( Log N),所以堆排序的时 间复杂度为:O(N*Log(N) )。
- 稳定性:不稳定
- 涉及到堆的一些性质,我在这不多说,我得博客中专门有一篇是在讲堆排序的,因此不多说。
//堆排序
void AdjustDown(int* array, int parent, int size)
{
int child = (parent << 1) + 1;
while (child < size)
{
if (child + 1 < size && array[child] < array[child + 1])
child += 1;
Swap(&array[child], &array[parent]);
parent = child;
child = (parent << 1) + 1;
}
}
void HeapSort(int* array, int size)
{
int root = ((size - 2) >> 1);
for (; root >= 0; root--)
AdjustDown(array, root, size);
for (int i = 0; i < size - 1; i++)
{
Swap(&array[0], &array[size - i - 1]);
AdjustDown(array, 0, size - i - 1);
}
}
交换排序
- 利用交换元素的位置进行排序的方法称作交换排序
- 常用的交换排序的方法:冒泡排序和快速排序
冒泡排序
- 冒泡排序最好情况时间复杂度O(n),冒泡排序最坏情况下时间复杂度O(n^2)
- 冒泡排序空间复杂度O(1)
- 冒泡排序是一种稳定的排序算法
- 冒泡排序是大家比较熟的排序方式了,年代也不比较久远了。但是这个方法还是值得我们学习的,就是两个循环嵌套。
//冒泡排序
void BubbleSort(int* array, int size)
{
int i, j, flag;
for (i = 0; i < size; i++)
{
flag = 1; //假设已经有序
for (j = 0; j < size - i - 1; j++)
{
if (array[j] > array[j + 1])
{
Swap(&array[j], &array[j + 1]);
flag = 0;
}
}
if (flag == 1)
break;
}
}
- 一点点的优化技巧,如果外层循环某次没有进行交换元素,那么一定是数组已经排好序了,所以我们设置一个标记 flag ,遇到没有交换的循环时,直接让他结束就好。
接下来就是重点了:非常厉害的快速排序
-
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
-
将区间按照基准值划分为左右两半部分的常见方式有:
- hoare版本
- 挖坑法
- 前后指针
-
1、在待排序的序列中找一个基准值----【left,right)–区间中最后一个元素
2、方法:按照基准值将序列分割成左右两个部分
3、排基准值的左边
4、排基准值的右边 -
时间复杂度
平均 O(nlogn) 最好 O(nlogn) 最坏 O(n^2) (但是我们会使用三数取中法,所以这种情况不会出现) -
空间复杂度
平均 O(logn) 最好 O(logn) 最坏 O(n) -
快排-----对于有序 || 数据接近有序-----退化成单支树–O(n) 这时候可以使用插入排序
-
算法是不稳定的
-
快速排序:如果在 有序 || 数据接近有序----退化成单支树—时间复杂度就变成O(N^2)
-
那么需要避免取到的基准值是最大值或者最小值。
-
如何降低基准值取到最大值或者取到最小值得概率:每次划分取到极大值或者极小值的概率。利用三数取中法。
-
使用场景:元素越乱越好
void _QuickSort(int* array, int left, int right)
{
if (left == right) {
// 区间内只剩一个数,有序,不需要再排序
return;
}
if (left > right) {
// 区间内没有数了,所以不需要排序
return;
}
int pivot = array[right]; // 选最右边的一个作为基准值
int div = Partion_3(array, left, right); // [div] 放基准值
_QuickSort(array, left, div - 1);
_QuickSort(array, div + 1, right);
}
void QuickSort(int* array, int size)
{
_QuickSort(array, 0, size - 1);
}
- 我写的这种快排采用递归的形式。那么需要每次找基准值的位置。我这儿共有三种找基准值位置的方法。
hoare版本的
- 采用左右指针的方式,定义两个指针,begin 指向最左边,end 指向最右边。然后将数组最后一个元素定义为基准值 datum 。先让begin 向右走,如果找到一个比基准值大的元素停下来,在让end 向左走,如果找到一个比基准值小的元素停下来,交换begin 和end 所指向的元素。如果碰到begin 等于 end 的话,那么我们就可以让循环结束了,循环结束后交换基准值和begin或者end所指向的元素,此时的begin 一定和end 相等。这样,我们的数组的一个元素的位置就已经确定好了。
//左右指针法
int Partion_1(int* array, int left, int right)
{
int begin = left, end = right;
int datum = array[right];
while (left < end)
{
while (left < end && array[begin] <= datum)
begin++;
if (begin == end)
break;
while (left < end && array[end] >= datum)
end--;
if (begin == end)
break;
Swap(&array[begin], &array[end]);
}
Swap(&array[begin], &array[right]);
return begin;
}
挖坑法:思想和 hoare 方法稍有不同。也是定义一个begin 让他指向left (数组最左边),再定义一个end 让他指向 right (数组最右边),还有定义一个基准值 datum ,起初让他拿到数组最右边的元素。接下来,让begin 从左往右走,直到遇见比datum 大的元素,停下来,将begin 所指向的元素赋给基准值 所指向的元素,那么在begin 处产生一个坑。然后让end 向左走,直到遇见比datum 小的元素,停下来,将end 所指向的元素赋给begin 所指向的元素,则在end 处产生一个新的坑,继续循环。当循环终止,也就是begin 等于 end 时,将基准值赋给begin或者end 所产生的坑,就可以将所有坑填好。最后返回begin 或者 end 的值,也就是位置。那么这个位置的数字就已经完成固定了。
int Partion_2(int* array, int left, int right)
{
int begin = left, end = right;
int datum = array[right];
while (begin < end)
{
while (begin < end && array[begin] <= datum)
begin++;
if (begin == end)
break;
array[end] = array[begin];
while (begin < end && array[end] >= datum)
end--;
if (begin == end)
break;
array[begin] = array[end];
}
array[begin] = datum;
return begin;
}
前后指针法:这种方法的代码是比较少的。首先定义一个div 和 cur 两个整形都让他指向数组的开始,基准值datum 还是数组最后一个元素的值。让cur 从数组开始到数组结束遍历,循环里面嵌套一个判断,如果cur 所指向的元素小于等于基准值,那么就交换cur 指向的元素和div 指向的元素,同时让div++;这样一层循环完成之后,就可以将比基准值小的元素放在比基准值大的元素之前。并且得到div的位置,接下来交换基准值的元素和div 所指向的元素,并且返回div 的位置。这样也可以一次将一个元素放到其准确的位置。
int Partion_3(int* array, int left, int right)
{
int div = left;
int cur = left;
int datum = array[right];
for (cur = left; cur < right; cur++)
{
if (array[cur] <= datum)
{
Swap(&array[cur], &array[div]);
div++;
}
}
Swap(&array[div], &array[right]);
return div;
}
- 快排的三种方法我介绍完了,还有一点就是我只给出了递归的方式,大家可以自行实现非递归的形式。提示:可以使用栈这个数据结构来完成非递归的形式。
归并排序
- 基本思想: 将待排序的元素序列分成两个长度相等的子序列,对每一个子序列排序, 然后将他们合并成一个序列。合并两个子序列的过程称为二路归并
- int array[] = {21, 25, 49, 25, 16, 8, 31, 41};
- 归并排序核心步骤:
- 分组
- 归并
- 由于归并排序不依赖于待排序元素序列的初始输入状态,每次划分时两个子序列的长度基本一致,所以归并排序的最好、最差和平均时间复杂度均 为O(N*Log2 N)
- 它是一种稳定的排序算法
void MergeData(int* array, int left, int middle, int right, int* temp)
{
int index = left;
int begin1 = left, end1 = middle, begin2 = middle, end2 = right;
while (begin1 < end1 && begin2 < end2)
{
if (array[begin1] < array[begin2])
temp[index++] = array[begin1++];
else
temp[index++] = array[begin2++];
}
while (begin1 < end1)
temp[index++] = array[begin1++];
while (begin2 < end2)
temp[index++] = array[begin2++];
}
void _MergeSort(int* array,int left, int right, int* temp)
{
if (right - left >1)
{
int middle = left + ((right - left) >> 1);
_MergeSort(array, left, middle, temp);
_MergeSort(array, middle, right, temp);
MergeData(array, left, middle, right, temp);
memcpy(array + left, temp + left, (right - left)*sizeof(int));
}
}
void MergeSort(int* array, int size)
{
int* temp = (int*)malloc(size*sizeof(int));
if (!temp)
{
assert(0);
return;
}
_MergeSort(array, 0, size, temp);
free(temp);
}
还有一些排序方式,比如非比较排序
- 非比较排序又有:
- 计数排序
- 计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用
- 操作步骤:
- 1、统计相同元素出现次数
- 2、根据统计的结果将序列回收到原来的序列中
- 基数排序
- 这两种方法我就不多说了,感兴趣的话自行学习。
各排序算法比较
如果想要了解更多关于排序这方面的知识,一定要看看这篇博客。
http://dsqiu.iteye.com/blog/1707423