目录
一、插入排序
1、直接插入排序
过程:假设序列:L[1...i-1] L[i] L[i+1...n],前Lq=L[1...i-1]有序,将L[i]与Lq从后往前比较,找到插入位置k,将L[k...i-1]的所有元素依次往后移一个元素,再复制。∴上述操作执行n-1次就能得到一个有序的表。
//直接插入排序
void InsertSort(int A[], int n) {//n个元素,0位不存放元素,哨兵
int i, j;
for (i = 2; i <= n; i++) //哨兵和第一位不需要参与比较
if (A[i] < A[i-1]) { //如果当前位置比前有序序列小,要排序
A[0] = A[i]; //当前位置存入哨兵
for (j = i - 1; A[0] < A[j]; j--) //开始排序
A[j + 1] = A[j];
A[j + 1] = A[0];
}
}
空间复杂度:O(1)
时间复杂度:在整个过程中,需要进行n-1趟操作。每趟操作都分为:比较和移动。比较和移动次数取决于排序表的初始状态。最好情况:已有序,不用移动,O(n)。 最坏情况:逆序,比较和移动次数都达到最大,O()。故平均时间复杂度:O(
)。
稳定性:稳定
适用性:适用于顺序存储和链式存储的线性表,采用链式存储时无需移动元素。
2、折半插入排序
在直接插入排序的基础上,把比较和移动操作分离。即先折半查找出元素的待插入位置,再统一移动待插入位置之后的所有元素
void InsertSort(int A[], int n) {
int i, j, low, high ,mid;
for (i = 2; i <= n; i++) {
A[0] = A[i];//哨兵
low = 1, high = i - 1;
//二分 最终high会在low的左边
while (low <= high) {
mid = (low + high) / 2;
if (A[mid] > A[0])high = mid - 1;
else low = mid + 1;
}
//移动元素
for (j = i - 1; j >= high + 1; j--)
A[j + 1] = A[j];
A[high + 1] = A[0];
}
}
空间复杂度:O(1)
时间复杂度:比较元素次数减少,时间复杂度为:O(),比较次数与待排序表的初始状态无关。移动元素次数依赖于待排序表的初始状态。平均时间复杂度:O(
)。对于数量不是很大的排序表,折半插入排序能表现出很好的性能。
稳定性:稳定
适用性:适用于顺序存储的线性表
3、希尔排序
把排序表分割成若干子表:L[i,i+d,i+2d....]为一个子表,对该子表进行直接插入排序,多次排序后,再对全体记录进行一次直接插入排序。
void ShellSort(int A[], int n) {
int d, i, j;//d为增量
for (d = n / 2; d >= 1; d /= 2)
for(i=d+1;i<=n;i++)//依次处理
if (A[i] < A[i - d]){ //直接插入排序 逆序则要排序 当前子表前两个数
A[0] = A[i];//不是哨兵,只是暂存
for (j = i - d; j > 0 && A[0] < A[j]; j -= d) //直接插入排序
A[j + d] = A[j];
A[j + d] = A[0];
}
}
空间复杂度:O(1)
时间复杂度:比较元素次数减少,时间复杂度为:O(),比较次数与待排序表的初始状态无关。移动元素次数依赖于待排序表的初始状态。平均时间复杂度:O(
)。对于数量不是很大的排序表,折半插入排序能表现出很好的性能。
稳定性:不稳定
适用性:适用于顺序存储的线性表
二、交换排序
1、冒泡排序
两个两个作比较。每一趟冒泡都能确定当前无序表的最小元素的位置。这样最多做n-1趟冒泡就能排好序。如果某趟冒泡结束后没有发生交换,说明已经有序,冒泡排序结束。
void BubbleSort(int A[], int n) {
for (int i = 0; i <n; i++) {//每一趟冒泡
bool flag = false;//暂定flag为没有发生交换
for (int j = n - 1; j > i; j--) {//i后面的数要排序
if (A[j - 1] > A[j]) {
swap(A[j], A[j - 1]);
flag = true;
}
}
if (flag == false)return;//没有发生交换,结束
}
}
空间复杂度:O(1)
时间复杂度:最好情况:比较次数为n-1,移动次数为0。时间复杂度为:O(n)。
最坏情况:逆序,要进行n-1趟排序。第i趟要进行n-i次比较。平均时间复杂度:O()。
稳定性:稳定
适用性:适用于顺序存储和链式存储的线性表
2、快速排序
基于分治的思想。取定x,通过排序,把所有小于x的数放在x的左边,所有大于等于x的数放在x的右边。而x现在的位置,就是x的最终的位置。再将左右两个子区间进行递归排序。直至每部分内只有一个元素或为空位置。每一趟排序后会将上一趟的基准元素归位。
void QuickSort(int A[], int low, int high) {//传参为下标
if (low >= high)return; //
int x = A[low + high >> 1]; //取中间数
int i = low - 1, j = high + 1;
while (i < j) {
do i++; while (A[i] < x);
do j--; while (A[j] > x);
if (i < j)swap(A[i], A[j]);
}
QuickSort(A, low, i - 1);//对x左边排序
QuickSort(A, i, high);//对x右边排序
}
空间复杂度:递归,容量与递归调用的最大层数一致。
最好情况:O()。//把n个元素组成二叉树,共h层=递归调用层数
最坏情况下,进行n-1次递归:O(n)。平均:O()
时间复杂度:快排的运行时间与划分是否对称有关,最坏情况为两个区域为n-1和0个元素。
最好情况:平衡划分。时间复杂度为:O()
最坏情况:基本有序或逆序,最坏情况下时间复杂度:O()。
稳定性:不稳定
适用性:适用于顺序存储的线性表
三、选择排序
1、简单选择排序
在无序表中,每一趟都把关键字最小的元素与L[i]交换,每一趟可以确定一个元素的最终位置。
void SelectSort(int A[], int n) {
for (int i = 0; i < n-1; i++) { //最后一个元素不用排序
int min = i;//把当前i指向的元素设为最小值
for (int j = i + 1; j < n; j++) //遍历后面的数
if (A[j] < A[min])min = j;//如果找到了比i更小的数,min指向这个更小的数
if (min != i)swap(A[i], A[min]);//如果最小数有改变,交换
}
}
空间复杂度:O(1)
时间复杂度:元素移动次数少,不超过3(n-1)次,最少为0次,此时已有序。但元素的比较次数与序列的初始状态无关,始终是n(n-1)/2次,故时间复杂度:O()。
稳定性:不稳定
适用性:适用于顺序存储和链式存储的线性表,以及关键字较少的情况
2、堆排序
将堆视为一颗完全二叉树,分为大根堆和小根堆(注意不是排序二叉树);
思路:1、建堆
2、根据堆的特点(以大根堆为例),堆顶为最大元素。输出堆顶元素后,通常将堆底元素送入堆顶,堆被破坏。
3、调整。
建堆+调整+排序:n个结点的完全二叉树,最后一个结点是第个结点的孩子。对以第
个结点为根的子树进行筛选,使该树成为堆。再向前依次对以各结点(
-1 ~ 1)为根的子树进行筛选。
//建堆
//处理每个非终端节点
void BuildMaxHeap(int A[], int len) {
for (int i = len / 2; i > 0; i++)
HeadAdjust(A, i, len);
}
//对以k为根的子树进行调整
void HeadAdjust(int A[], int k, int len) {
A[0] = A[k];//暂存子树的根节点
for (int i = 2 * k; i <= len; i*=2) {//i为左孩子,沿关键字较大的子节点向下筛选
if (i < len && A[i] < A[i + 1]) //如果k有左右孩子结点并且右孩子大于左孩子
i++;//i移动到右孩子
if (A[0] >= A[i])break;//如果根节点大于右孩子,则也大于左孩子,不需要调整
else {//调整
A[k] = A[i];//将A[i]调整到双亲结点上
k = i;//继续向下筛选
}
}
A[k] = A[0];//被筛选的结点最终位置
}
//一颗完全二叉树,先从最后一层最后一个子树进行筛选。
// 将最后一层筛选后,再进行倒数第二层的筛选,同时会检查某第二层结点调整后,该结点是否还符合堆的特性,并依次向下筛选
//排序
void HeapSort(int A[], int len) {
BuildMaxHeap(A, len);//建堆
for (int i = len; i > 1; i--) {//n-1趟的交换和建堆的过程
swap(A[i], A[1]);//输出堆顶元素(和堆底交换)
HeadAdjust(A, 1, i - 1);//调整
}
}
调整时间和树的高度有关,为O(h)。
在建含n个元素的堆时,比较总次数不会超过4n,为O(n)。说明在线性时间内可以建一个堆
一个结点,每下坠一层,最多只需对比关键词2次。若树高为h,某结点在第i层,则将该结点向下调整最多只需要下坠h-i层,关键字对比不超过2(h-i)次。
n个结点的完全二叉树树高h=,第i层最多有
个结点,而只有第1~(h-1)层的结点可能要下坠。
空间复杂度:O(1)
时间复杂度:建堆时间为O(n),之后有n-1次向下调整操作,调整为O(h)。所以在最好/坏/平均情况下,为O()
稳定性:不稳定
适用性:适用于顺序存储的线性表,以及关键字较多的情况
堆的插入操作:先将新节点放在堆的末端,再对这个结点进行向上操作(向上操作时,只对该结点的路线进行调整,其他子树不需要调整)。(向上调整算法,比较次数最多等于树的高度-1,因为树的高度为,所以堆的向上调整算法的比较次数最多等于
)
堆的删除操作:删除结点后用堆底端元素进行代替,再调整。
四、归并排序
将无序表视为n个有序的子表,每个子表长度为1,然后两两归并,得到个长度为2或1的有序表,再继续两两归并.....直到合成一个长度为n的有序表,这叫二路归并。m路归并:每选出一个元素需要对比关键字m-1次。
int* B = (int*)malloc((n + 1) * sizeof(int));//辅助数组
void MergeSotr(int A[], int l, int r) {
if (l >= r)return;//递归结束条件
int mid = l + r >> 1;
MergeSotr(A, l, mid), MergeSotr(A, mid + 1, r);//递归
int k = 0, i = l, j = mid + 1;//分为两个有序数组,进行整合
while (i <= mid && j <= r) {
if (A[i] <= A[j])B[k++] = A[i++];
else B[k++] = A[j++];
}
while(i<=mid)B[k++] = A[i++];//有一方已经指到最后,剩余的直接放到B里
while(j<=r)B[k++] = A[j++];
for (i = l, j = 0; i <= r; i++, j++)A[i] = B[j];//恢复A数组
}
对于N个元素进行k路归并,排序的趟数m满足=N,从而m=
,考虑到m为整数,因此m=
。
空间复杂度:O(n)
时间复杂度:每趟归并的时间为O(n),共需要趟归并,所以时间复杂度为O(
)。
稳定性:稳定
适用性:适用于顺序存储和链式存储的线性表
五、基数排序
借助多关键字排序的思想对单逻辑关键字进行排序。分为最高位优先和最低位优先。按关键字位权重依次逐层划分为若干更小的子序列,最后将所有子序列依次连接成一个有序序列。
空间复杂度:r个队列:r个对头和队尾指针。O(r)
时间复杂度:有d趟分配和收集操作。一趟分配需要遍历所有关键字,O(n)。一趟收集需要合并r个队列,O(r),因此时间复杂度为O(d(r+n))。且它与序列的初始状态无关。
稳定性:稳定
适用性:适用于顺序存储和链式存储的线性表
六、计数排序
七、比较和应用