赞
踩
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
注:核心思想是把一个数插入一段有序区间,保持这段区间有序
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
直接插入排序的实现
void InsertSort(int* a, int n) { for (int i = 0; i < n-1; ++i) //当i=n-2时,表示前n-1个数有序,将数组下标为n-1的数进行插入 { // 单趟插入 int end = i; int tmp = a[end + 1]; while (end >= 0) { if (tmp < a[end]) { a[end + 1] = a[end]; --end; } else { break; } } a[end + 1] = tmp; //无论是end<0时(有序区间的所有数都大于该数)跳出循环还是tmp >= a[end](有序区间的所有数都小于该数或者有序区间的一部分数都大于该数)时跳出循环,都需要在将要插入的数据,插入到有序区间内。 } }
直接插入排序的特性总结
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
希尔排序是先对间隔为gap(gap是几就会分成几组)的分成一组,然后按分组对这gap组数据进行直接插入排序(这个过程是预排序(可以排多趟的),使这组一组数列接近顺序有序),然后再用直接插入排序,使得数列为有序。
注:
希尔排序的实现
void ShellSort(int* a, int n) { // gap > 1 预排序 // gap == 1 直接插入排序 int gap = n; while (gap > 1) //这部分时间复杂度为O(logN) { gap = gap / 3 + 1; //推荐这种写法(经过实验测验的) //gap = gap / 2; 这个方法也可以不过需要n大于1的,这种写法比上面的写法的预排序次数要多 //间隔为gap的多组并排 //该循环控制的是同时走这个gap组数据间隔为gap的插入排序 for (int i = 0; i < n - gap; ++i) //这部分时间复杂度为O(N)(无论是gap很大、无序时(循环体内容的时间复杂度为常数而循环次数的时间复杂度为O(N))还是gap很小,接近有序时都是O(N)) { //对间隔为gap的一组数列进行直接插入排序 //分组为gap的单趟排序 int end = i; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end]) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = tmp; } } }
希尔排序的特性总结
《数据结构(C语言版)》–严蔚敏
希尔排序的分析是一个复杂的问题,因为它的时间是所取“增量”序列的函数,这涉一些数学上尚未解决的难题。因此,到目前为止尚未有人求得一种最好的增量序列,但大量的研究已得出一些局部的结论。如有人指出,当增量序列为dlta[k]=
2
t
−
k
+
1
2^{t-k+1}
2t−k+1-1时,希尔排序的时间复杂度为O(
n
3
/
2
n^{3/2}
n3/2),其中t为排序趟数,1 ≤ k ≤ t ≤
l
o
g
(
n
+
1
)
log(n+1)
log(n+1)。还有人在大量的实验基础上推出:当n在某个特定范围内,希尔排序所需的比较和移动次数约为
n
1.3
n^{1.3}
n1.3,当n
→
\rightarrow
→
∞
\infty
∞时,可减少到n
(
(
(logn
)
2
)^2
)2。增量序列可以有各种取法,但需注意:应使增量序列中的值没有除1之外的公因子,并且最后一个增量值必须等于1。
《数据结构-用面相对象方法与C++描述》— 殷人昆
gap的取法有多种。最初Shell提出取 gap =n/2 ,gap = gap/2 ,直到 gap = 1,后来Knuth提出取gap = (gap/3) +1。还有人提出都取奇数为好,也有人提出各gap互质为好。无论哪一种主张都没有得到证明。对希尔排序的时间复杂度的分析很困难,在特定情况下可以准确地估算关键码的比较次数和对象移动次数,但想要弄清关键码比较次数和对象移动次数与增量选择之间的依赖关系,并给出完整的数学分析,还没有人能够做到。在Knuth所著的《计算机程序设计技巧》第3卷中,利用大量的实验统计资料得出,当n很大时,关键码平均比较次数和对象平均移动次数大约在
n
1.25
n^{1.25}
n1.25到1.6
n
1.25
n^{1.25}
n1.25范围内,这是在利用直接插入排序作为子序列排序方法的情况下得到的。
因为咱们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O(
n
1.25
n^{1.25}
n1.25)到O(1.6
n
1.25
n^{1.25}
n1.25)来算。
基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接选择排序的实现
void SelectSort(int* a, int n) { int begin = 0, end = n - 1; while (begin < end) { //选出在begin和end区间内最小、大的数 int mini = begin, maxi = begin; for (int i = begin; i <= end; ++i) { if (a[i] > a[maxi]) maxi = i; if (a[i] < a[mini]) mini = i; } Swap(&a[begin], &a[mini]); // 如果begin和maxi重叠,需要修正一下新的maxi的位置 if (begin == maxi) { maxi = mini; } Swap(&a[end], &a[maxi]); ++begin; --end; } }
直接选择排序的特性总结
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
堆排序的实现
void AdjustDown(int* a, int n, int parent) { 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; } } } // 堆排序 O(N*logN) void HeapSort(int* a, int n) { // 建大堆 O(N) for (int i = (n - 1 - 1) / 2; i >= 0; --i) { AdjustDown(a, n, i); } // O(N*logN) int end = n - 1; while (end > 0) { Swap(&a[0], &a[end]); AdjustDown(a, end, 0); --end; } }
堆排序的特性总结
详解见堆排序
基本思想
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序的实现
//方法一 void BubbleSort(int* a, int n) { for (int j = 0; j < n; ++j) { int exchange = 0; for (int i = 1; i < n-j; ++i) { if (a[i - 1] > a[i]) { Swap(&a[i - 1], &a[i]); exchange = 1; } } if (exchange == 0) { break; } } } // 时间复杂度:O(N^2)(最坏) // 最好:O(N) //方法二 void BubbleSort(int* a, int n) { for (int end = n; end > 0; --end) { int exchange = 0; for (int i = 1; i < end; ++i) { if (a[i - 1] > a[i]) { Swap(&a[i - 1], &a[i]); exchange = 1; } } if (exchange == 0) { break; } } }
注:
冒泡排序的时间复杂度为O( N 2 N^2 N2)(最坏情况),当用冒泡排序排有序序列时时间复杂度为O(N)(最好情况)
详解见冒泡排序
冒泡排序的特性总结
注:
基本思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
注:
将区间按照基准值划分为左右两半部分的常见方式有
基本思想:
Hoare版本的单趟排序的基本步骤如下:
Hoare版本的单趟实现:
int PartSort(int* a, int left, int right) { int midi = GetMidIndex(a, left, right); Swap(&a[left], &a[midi]); int keyi = left; while (left < right) { // 左边做key,右边先走找小 while (left < right && a[right] >= a[keyi]) { --right; } // 左边再走,找大 while (left < right && a[left] <= a[keyi]) { ++left; } // 交换,把大的换到右边,小的换到左边 Swap(&a[left], &a[right]); } Swap(&a[keyi], &a[left]); return left; }
注:
hoare方法的单趟有不少容易出错细节,有人就进行了优化,给了两种方式分别是挖坑法以及前后指针。
基本思想:
挖坑法的单趟排序的基本步骤如下:
选出一个数据(一般是最左边或是最右边的)存放在key变量中,在该数据位置形成一个坑。
还是定义一个L和一个R,L从左向右走,R从右向左走。(若在最左边挖坑,则需要R先走;若在最右边挖坑,则需要L先走)。
在走的过程中,若R遇到小于key的数,则将该数抛入坑位,并在此处形成一个坑位,这时L再向后走,若遇到大于key的数,则将其抛入坑位,又形成一个坑位,如此循环下去,直到最终L和R相遇,这时将key抛入坑位即可。(选取最左边的作为坑位)
经过一次单趟排序,最终也使得key左边的数据全部都小于key,key右边的数据全部都大于key。
注:挖坑法也是选择一个值作为基准值,可以选择最左边的也可以选择最右边的
挖坑法的单趟实现:
// 挖坑法 int PartSort(int* a, int left, int right) { int midi = GetMidIndex(a, left, right); Swap(&a[left], &a[midi]); int key = a[left]; int hole = left; while (left < right) { // 右边去找小,填到左边的坑里面 while (left < right && a[right] >= key) { --right; } // 把右边找的小的,填到左边的坑,自己形成新的坑 a[hole] = a[right]; hole = right; // 左边去找大,填到右边的坑里面 while (left < right && a[left] <= key) { ++left; } // 把左边找的大的,填到右边的坑,自己形成新的坑 a[hole] = a[left]; hole = left; } //将所选择的基准值填到两个指针相遇的位置(坑)上 a[hole] = key; return hole; }
前后指针法的单趟排序的基本步骤如下:
选出一个key,一般是最左边或是最右边的。
起始时,prev指针指向序列开头,cur指针指向prev+1。
若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++;若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur指针越界,此时将key和prev指针指向的内容交换即可。
经过一次单趟排序,最终也能使得key左边的数据全部都小于key,key右边的数据全部都大于key。
注:
前后指针版本的单趟实现:
int PartSort(int* a, int left, int right) { int midi = GetMidIndex(a, left, right); Swap(&a[left], &a[midi]); int keyi = left; int prev = left; int cur = prev + 1; while (cur <= right) { if (a[cur] < a[keyi] && ++prev != cur) //++prev != cur这个条件解决的是prev指针和cur指针所指向的相同位置上的值互相交换的问题 Swap(&a[prev], &a[cur]); ++cur; } Swap(&a[prev], &a[keyi]); return prev; }
快速排序的递归实现
首先通过单趟划分出左子序列和右子序列,与此同时也将基准值放到了正确的位置,然后通过递归(分治的思想)再将左子序列和右子序列分别划分出左子序列和右子序列,直到左子序列和右子序列中只有一个值或者不存在则终止划分,此时通过分治的思想将左子序列和右子序列实现有序,从而整个数组也达到有序。
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort(a, left, right);
// [left, keyi-1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
注:快速排序的时间复杂度为O(N l o g N logN logN)(理想情况),在最坏情况下(有序数列)快速排序的时间复杂度为O( N 2 N^2 N2)。
快速排序优化
三数取中法的实现快排:
通过三数取中法把所选的基准值的下标求出来,然后通过交换的方式让最左边位置上的值作为基准值(如果是有序就全部二分了,每次选到的都是中位数,此时就解决了快排面临有序的情况),最后按照上面快排的思路执行即可。
// 三数取中法顾名思义取最左边、最右边、中间这三个值的中间值的下标 int GetMidIndex(int* a, int left, int right) { //int mid = (left + right) / 2; //不建议这样写,这样写存在bug,如果left超过整体的一半、right也超过整体的一半,它们两加到一起就溢出了(这里的溢出指的是int的取值范围可能会溢出),当然要使用这种写法可以将参数类型改为无符号整型 int mid = left + (right - left) / 2; if (a[left] < a[mid]) { if (a[mid] < a[right]) { return mid; } else if (a[left] < a[right]) { return right; } else { return left; } } else //(a[left] > a[mid]) { if (a[mid] > a[right]) { return mid; } else if (a[left] < a[right]) { return left; } else { return right; } } } int PartSort(int* a, int left, int right) { int midi = GetMidIndex(a, left, right); Swap(&a[left], &a[midi]); int keyi = left; while (left < right) { // 左边做key,右边先走找小 while (left < right && a[right] >= a[keyi]) { --right; } // 左边再走,找大 while (left < right && a[left] <= a[keyi]) { ++left; } // 交换,把大的换到右边,小的换到左边 Swap(&a[left], &a[right]); } Swap(&a[keyi], &a[left]); return left; } void QuickSort(int* a, int left, int right) { if (left >= right) return; int keyi = PartSort2(a, left, right); QuickSort(a, left, keyi - 1); QuickSort(a, keyi + 1, right); }
注:快速排序的时间复杂度有了三数取中就不用看最坏情况了(不会出现最坏情况),其时间复杂度为O(N l o g N logN logN)。
递归到小的子区间时,可以考虑使用插入排序:
快速排序非递归
递归程序相比循环迭代程序的问题:
递归的深度太深,可能会导致栈溢出
性能问题(现在编译优化得很好,问题不大),递归会有一些性能的开销(建立栈帧,现在编译器优化再加上CPU速度提升,递归的这个问题相较于循环并不会很差)
注:
基本思路: 首先创建一个栈,其次要对排序数组的区间进行入栈,然后出栈取数组区间,对该区间的数据进行单趟排序,单趟排完序以后基准值就放到正确的位置上了,根据基准值将这段区间划分为左子序列(小于基准值的)和右子序列(大于基准值的),如果左子序列这段区间只有一个值或者这段区间不存在就不需要将这段区间入栈,反之入栈,而右子序列也是如此,如此往复直到栈为空就停止,此时该数组已经有序。(依次把需要单趟排的区间入栈,依次取栈里面的区间出来单趟排,再把需要处理得子区间入栈)
快速排序非递归的实现:
void QuickSortNonR(int* a, int left, int right) { ST st; StackInit(&st); StackPush(&st, right); StackPush(&st, left); while (!StackEmpty(&st)) { int begin = StackTop(&st); StackPop(&st); int end = StackTop(&st); StackPop(&st); int keyi = PartSort(a, begin, end); // [begin, keyi-1] keyi [keyi+1, end] if (keyi+1 < end) { StackPush(&st, end); StackPush(&st, keyi+1); } if (begin < keyi-1) { StackPush(&st, keyi-1); StackPush(&st, begin); } } StackDestroy(&st); }
快速排序的特性总结
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
基本思想
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
归并排序的递归实现
首先对数组进行划分计算出数组的中间值下标从而分割成两段区间,然后左右区间的数组也按照递归的方式对该区间进行划分(注意当区间中只有一个值或者区间不存在则直接返回),直到递归回来的左右区间有序则进行归并,最后将归并后的结果拷贝到原数组中即可。
void _MergeSort(int* a, int left, int right, int* tmp) { if (left >= right) return; int mid = (right + left) / 2; // [left, mid][mid+1, right] _MergeSort(a, left, mid, tmp); _MergeSort(a, mid+1, right, tmp); // 归并 int begin1 = left, end1 = mid; int begin2 = mid+1, end2 = right; int index = left; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) { tmp[index++] = a[begin1++]; } else { tmp[index++] = a[begin2++]; } } while(begin1 <= end1) { tmp[index++] = a[begin1++]; } while (begin2 <= end2) { tmp[index++] = a[begin2++]; } // 归并后的结果,拷贝回到原数组 for (int i = left; i <= right; ++i) { a[i] = tmp[i]; } } void MergeSort(int* a, int n) { int* tmp = (int*)malloc(sizeof(int)*n); _MergeSort(a, 0, n - 1, tmp); free(tmp); }
注:
归并排序的非递归实现
一个数已经是不可再分割的一个子单元,数组进行归并排数时可以一个和一个进行归并从而数组中的数据就可以达到两两有序,接着再两个和两个进行归并从而数组中的数据就可以达到每四个有序,依次类推,直到有序数列的个数大于等于数组的个数终止归并即可。
依次循环控制归并个数(eg:一个一个的归并、两个两个的归并、四个四个的归并、……(不够也是可以归并的))
void MergeSortNonR(int* a, int n) { int* tmp = (int*)malloc(sizeof(int)*n); int groupNum = 1; while (groupNum < n) { for (int i = 0; i < n; i += 2 * groupNum) //i控制的是数组中每两组数据归并的起始位置 { // 两组数据的进行归并的区间 [begin1][end1] [begin2,end2] // 归并 int begin1 = i, end1 = i + groupNum - 1; int begin2 = i + groupNum, end2 = i + groupNum * 2 - 1; int index = begin1; // 数组数据个数,并不一定是按整数倍,所以划分的分组可能越界或者不存在 // 1、[begin2,end2] 不存在, 修正为一个不存在的区间 if (begin2 >= n) { begin2 = n + 1; end2 = n; //这种处理是为了防止tmp数组中存在随机值(将归并好的数组拷贝到原数组中去) //这种可能存在一个一个归并的情况 } // 2、end1越界,修正一下 if (end1 >= n) { end1 = n - 1; //这种处理是为了防止tmp数组中存在随机值(将归并好的数组拷贝到原数组中去) } // 3、end2越界,需要修正后归并 if (end2 >= n) { end2 = n - 1; } while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) { tmp[index++] = a[begin1++]; } else { tmp[index++] = a[begin2++]; } } while (begin1 <= end1) { tmp[index++] = a[begin1++]; } while (begin2 <= end2) { tmp[index++] = a[begin2++]; } } // 拷贝回原数组 for (int i = 0; i < n; ++i) { a[i] = tmp[i]; } groupNum *= 2; //PrintArray(a, n); } free(tmp); }
注:
归并排序的特性总结:
基本思想
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤
注:
从而达到排序的目的,统计次数的数组的下标是依次递增的,遍历统计元素出现次数的数组就是按照大小来进行遍历的,往原数组写进去的值是从小到大排列的
计数排序如果差的值太大就不太实用了,这个排序是具有局限性的,并不是实用度很广,它适用于整数并且这些整数很集中,有许多重复的,范围也很集中的整数。
统计次数的数组不应该按0到最大值的区间去开辟数组,应该按照最小值到最大值的区间去开辟数组
计数排序中的统计次数用的映射(哈希映射)(可以是相对映射(映射位置是原来数组中的值去减最小值)或者绝对映射(值是多大就映射哪个位置),绝对映射可能会存在一定的空间浪费
计数排序的实现
void CountSort(int* a, int n) { int min = a[0], max = a[0]; for (int i = 1; i < n; ++i) { if (a[i] < min) { min = a[i]; } if (a[i] > max) { max = a[i]; } } int range = max - min + 1; int* count = (int*)calloc(range, sizeof(int)); // 统计次数 for (int i = 0; i < n; ++i) { count[a[i] - min]++; } // 根据count数组排序 int i = 0; for (int j = 0; j < range; ++j) { while (count[j]--) { a[i++] = j + min; } } }
注:
计数排序的特性总结
基本思想
按照整数的每个位数分组。在分组过程中,对于不足位的数据用0补位。分别按个、十、百、千、万……位排序再将各组连接起来,依次类推,直到按数组中元素的最高位对每个元素进行排序后,再将各组连接起来,便得到一个有序序列。
基数排序的实现
void RadixSort(int* a, int n) { int* tmp = (int*)calloc(n, sizeof(int)); //创建临时数组 int i, maxVal = 0, digit; for (i = 0; i < n; i++) //寻找最大元素 { if (a[i] > maxVal) maxVal = a[i]; } for (digit = 1; maxVal / digit > 0;digit *= 10) //LSD(基数排序从低位到高位排序,而MSD基数排序从高位到低位排序),从最低位开始,循环至最高位 { int count[10] = {0}; //创建10个桶,这十个分别代表位上的0-9 for (i = 0; i < n; i++) { count[a[i] / digit % 10]++; //a[i] / digit % 10该操作的目的是获取数组中每个元素所在digit位上的值 //统计对应位上值的数组元素的个数 } //通过count数组中每个元素的值建立原数组与tmp数组中相应元素位置的映射 for (i = 1; i < 10; i++) { count[i] += count[i - 1]; } //将数组中每个元素所在digit位上的值排序放到tmp数组中 for (i = n - 1; i >= 0; i--) { tmp[--count[a[i] / digit % 10]] = a[i]; } //将排好数的tmp数组拷贝到原数组中去 for (i = 0; i < n; i++) { a[i] = tmp[i]; } } }
基数排序的特性总结
注:了解即可
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O( N 2 N^2 N2) | O(N) | O( N 2 N^2 N2) | O(1) | 稳定 |
简单选择排序 | O( N 2 N^2 N2) | O( N 2 N^2 N2) | O( N 2 N^2 N2) | O(1) | 不稳定 |
直接插入排序 | O( N 2 N^2 N2) | O(N) | O( N 2 N^2 N2) | O(1) | 稳定 |
希尔排序 | O(N l o g N logN logN) ∽ \backsim ∽O( N 2 N^2 N2) | O( N 1.3 N^{1.3} N1.3) | O( N 2 N^2 N2) | O(1) | 不稳定 |
堆排序 | O(N l o g N logN logN) | O(N l o g N logN logN) | O(N l o g N logN logN) | O(1) | 不稳定 |
归并排序 | O(N l o g N logN logN) | O(N l o g N logN logN) | O(N l o g N logN logN) | O(N) | 稳定 |
快速排序 | O(N l o g N logN logN) | O(N l o g N logN logN) | O( N 2 N^2 N2) | O( l o g N logN logN) ∽ \backsim ∽O(N) | 不稳定 |
注:数组中存在多个相同的值如果排完序之后可能会导致相同的值的相对顺序交换就是不稳定;如果保证不交换那么就是稳定的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。