类别
1,插入类排序
在已有的有序序列中,通过插入新的关键词进行排序。有直接插入排序、折半插入排序和希尔排序。
2,交换类排序
以交换为核心,每一趟都通过一系列的交换动作,让一个关键词排到它的位置。有冒泡排序和快速排序
3,选择类排序
以选择为核心,每一趟都选出一个最大或最小的关键词,把它和有序序列中第一个或最后一个关键词交换,使选出的关键词在合适的位置。有简单选择排序和堆排序。
4,归并类排序
将两个或两个以上的有序序列合并为一个新的有序序列。有K路归并排序。
5,基数排序
把一个逻辑关键字拆分为多个关键字,然后对多各关键词进行排序。
插入类排序
直接插入排序
思路
在排序中,序列为以下状态:
有序 | 待排 | 无序 |
---|---|---|
L[1,…,i-1] | L[i] | L[i+1,…,n] |
要将L[i]插入有序序列L[1,…,i-1]中
1,在L[1,…,i-1]中找到L[i]的插入位置K(从后向前遍历,K>=L[i])
2,将L[k,…,i-1]全部后移一位,空出L[k]
3,将L[i]插入L[K]
初始时,我们将L[1]看做有序,依次对L[2]-L[n]执行n-1次插入排序
代码
//直接插入排序
void InsertSort(int A[], int n) {
int i, j, temp;
for (i = 1; i < n; ++i)//从第二个元素开始遍历
{
if (A[i] < A[i - 1]) {//若当前遍历的元素>=前一元素,已经有序故不操作;若<前一元素,则执行
temp = A[i];//暂存元素值
for (j = i - 1; j >= 0&&A[j]>temp; --j)//从前一元素,向前遍历有序序列,直到遇到大于等于该元素的,或者遍历到头
A[j + 1] = A[j];
A[j + 1] = temp;//插在A[J]后面(若A[j]==temp,保证了稳定性)
}
}
}
算法效率
1,空间复杂度:O(1)
只占用几个常量,空间复杂度不计算输入元素大小。
2,时间复杂度:O(
n
2
n^{2}
n2)
从第二个元素开始要循环n-1次。
每次循环最主要的开销是对比元素大小和移动元素。最坏的情况,逆序序列,每次要都要和已排序的所有元素对比,且有序序列元素都要后移。第i次循环,有序序列有i个元素,要对比i次,移动i+1次(包括待排序元素本身一次)。
则总体时间复杂度为O(
n
2
n^{2}
n2)
3,稳定性:稳定
从第二个元素开始遍历
if (A[i] < A[i - 1]) //若当前遍历的元素>=前一元素,已经有序,不操作;等于号保证了稳定性。若<前一元素,则执行。
链表
思路
对数组插入排序时,是两重循环,第一重是从第二个元素开始遍历整个数组,第二个是反向循环,从当前元素开始向前对已排序列遍历,第一个小于待排元素的元素,并插到它后面。
但单链表无法反向,所以第二重循环就可以对已排序列进行正向遍历,找到第一个大于当前结点值的结点,并插到它前面。
首先将L断开,L只保留一个节点。其余节点由p指向为待排序列。
遍历L找到第一个大于等于待排元素的节点,将待排元素插在前面,或者遍历完已排序列,则插在已排序列末尾。
带头结点代码
void select_sort(Listnode*& L)
{
Listnode *p,*pLate,*m,*prem;
if(L->next!=NULL)
{
p=L->next-next;//待排序列
L->next->next=NULL;//已排元素
while(p!=NULL){
prem=L;
m=prem->next;//遍历已排元素
while(m!=NULL&&m->data<p->data){//在已排序列找到第一个大于等于待排元素的元素。
prem=m;
m=m->next;
}
//将待排元素插入找到的元素之前
pLate=p->next;//从待排序列取出元素
p->next=prem->next;//插在找到的元素前面
prem->next=p;
p=pLate;//指针重新指向待排序列第一个元素
}
}
}
时间复杂度
对于每次循环来说
1,移动元素的次数减少,只需改动指针,无需把每个元素后移来插入。
2,关键字对比仍然是O(
n
2
n^{2}
n2)级别。因为链表只能从头到尾遍历,无法随机访问。这样每次都要从头到尾遍历已排序的序列,找到>=待排元素或者遍历完已排序列。
折半插入排序
思路
在直接插入排序的基础上做改进,以减少比较次数。
有序 | 待排 | 无序 |
---|---|---|
L[1,…,i-1] | L[i] | L[i+1,…,n] |
1,对已排序列L[1,…,i-1]使用折半查找法找到比待排元素L[i]大的元素L[k]
2,将L[k,…,i-1]全部后移一位,空出L[k]
3,将L[i]插入L[K]
折半查找:将待排序元素与有序序列中间元素((low+high)/2)对比,若小于就查中间元素左边序列(high=mid-1),大于等于就查找右边序列(low=mid+1)。
最后low指向的元素就是大于待排元素的最小元素,也就是说[low,i-1]均大于待排元素;而[0,high]小于等于待排元素,这时只需将待排元素插入low前即可。
代码
//折半插入排序
void InsertSort(int A[], int n) {
int i, j, low,high,mid,temp;
for (i = 1; i < n; ++i)//从第二个元素开始遍历
{
temp = A[i];//暂存数据
low = 0; high = i - 1;//已排序序列首尾元素
while (low <= high) {//折半查找
mid = (low + high) / 2;//取中间元素
if (A[mid] > temp)//不断缩小查找访问
high = mid - 1;
else
low = mid + 1;//low会不断指向值更大的方向
}
若j<low,待排元素比前面所有元素都大,无需后移,直接插在原处。
for (j = i - 1; j >= low; --j)//值大于temp的元素,依次后移
A[j + 1] = A[j];
A[low] = temp;//插入low位置
}
}
算法效率
1,空间复杂度:O(1)
只占用几个常量,空间复杂度不计算输入元素大小。
2,时间复杂度:O(
n
2
n^{2}
n2)
与直接插入排序算法相比,折半插入排序
在关键字对比方面,由于采取了折半查找法,时间大大减少。且每次比较的次数都固定,和初试序列无关,都是在low>high时结束。
在关键词移动方面,则和直接插入一样。
因此时间复杂度是O(
n
2
n^{2}
n2)
3,稳定性:稳定
将待排序元素与有序序列中间元素((low+high)/2)对比,若小于就查中间元素左边序列(high=mid-1),大于等于就查找右边序列(low=mid+1)。等于保证左边的序列始终大于等于待排元素,保证了稳定性。
希尔排序
思路
插入排序适合基本有序的序列,若序列逆序时间复杂度达到
O
(
n
2
)
O(n^{2})
O(n2),若为正序则可提升到O(n),希尔排序(缩小增量排序)就是在此基础上进行优化。
首先确定一个小于n的增量d,将序列分为形如L[i,i+d,…,i+kd]的d个子表。即间隔d的元素组成子表,然后对每个子表进行插入排序。不断缩小d的值,进行排序。当整个表已经基本有序(具有较好的局部有序性)的时候(d=1),最后进行一次排序即可。
代码
void ShellSort(int A[], int n) {
int d, i, j, k, temp;
for (d = n / 2; d >= 1; d = d / 2)//取增量,以增量为间隔构建子表
{
for (i = 0; i < d; ++i)//遍历子表的头结点
{
for (j = i + d; j < n; j = j + d)//遍历每个子表的元素,默认第一个元素有序,按直接排序处理。
{
if (A[j] < A[j - d]) {
temp = A[j];//暂存数据
for (k = j - d; k >= 0 && temp < A[k]; k -= d)
A[k + d] = A[k];
A[k + d] = temp;
}
}
}//每次都使整个序列更有序,从而直接排序速度越来越快。
}
}
算法效率
1,空间复杂度:O(1)
只占用几个常量,空间复杂度不计算输入元素大小。
2,时间复杂度:O(
n
2
n^{2}
n2)
根据增量的不同,会有不同的时间复杂度
d=d/2时是O(
n
2
n^{2}
n2)
d=
2
k
+
1
2^{k}+1
2k+1是O(
n
1.5
n^{1.5}
n1.5)
3,稳定性:不稳定
当值相同的值,划分到不同子表后,可能导致相对顺序变化
交换类排序
冒泡排序
思路
从后往前两两比较相邻元素的值,若为逆序(A[i-1]>A[i]),则交换两元素,直到序列比较完毕。每趟排序都将未排序列中最小元素放在已排序列的最终位置。
最多执行n-1次,若某一趟排序中没有进行任何交换,说明序列已经有序,则停止排序。
排序过程中,若两元素相同,则不交换,保证稳定性。
代码
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
//冒泡排序
void BubbleSort(int A[], int n) {
for (int i = 0; i < n-1; ++i) {
int flag = 1;
for (int j = n - 1; j > i; --j) {
if (A[j - 1] > A[j])
{
swap(A[j - 1], A[j]);
flag = 0;
}
}
if (flag == 1)
{
return;
}
}
}
链表实现
思路
数组是从后往前冒泡,而链表特别是单链表不方便从后向前查找,因此可以从前向后把较大的值冒到后面去。
链表排序最好还是要交换节点,而不只是交换值。
交换节点后p指针等于后移了一位
1,交换节点后
指针p与q(p的后一元素指针)交换了位置,实际相当于p已经后移一位,
2,不交换节点
当前元素小于下一元素,p需要手动后移,指向下一个元素(更大的)
3,pre位置不变
无论是否交换,pre指针位置不变,因此通过pre执行后移。
代码
//定义链表结构体
typedef struct Linklist
{
int data;
struct Linklist* next;
}Linklist;
//交换链表中两个节点
void swap(Linklist* pre,Linklist* p, Linklist* q)
{
pre->next = q;
p->next = q->next;
q->next = p;
};
//输出链表节点
void print(Linklist* L)
{
Linklist* p = L;
for (int i = 0; i < L->data; ++i)//L为头结点不能动 否则每次遍历就会改变条件
{
printf("%d ", p->next->data);
p = p->next;
}
printf("\n");
}
//简单选择排序(升序)
void Bubblesort(Linklist* head)
{
for (int i = head->data - 1; i > 0; --i)
{
int flag = 1;
Linklist* pre = head;
Linklist* p = pre->next;
for (int j = 0; j < i; ++j)//从第一个元素开始遍历到n-1个元素
{
if (p->data > p->next->data) {//将大的元素冒泡上去
swap(pre, p, p->next);//元素交换后,p指针就相当于前移了一次(数组中只交换值,不改变下标)
flag = 0;
}
pre = pre->next;//不管交换与否,pre位置不变,因此通过pre指针指行后移
p = pre->next;
}
if (flag == 1)
return;
}
}
算法效率
1,空间复杂度:O(1)
只占用几个常量,空间复杂度不计算输入元素大小。
2,时间复杂度:O(
n
2
n^{2}
n2)
最好情况:时间复杂度是O(
n
n
n),序列有序。只需只需一趟,对比n-1次,flag==false即结束算法。
最坏情况:时间复杂度是O( n 2 n^{2} n2),序列逆序。需要执行n-1趟,第i趟排序要执行n-i次对比,每次交换则需要执行3次。
3,稳定性:稳定
if (A[j - 1] > A[j])保证了,相同元素不会发生交换,从而保证了稳定性。
4,全局有序性
冒泡排序产生的有序序列是全局有序的,也就是说有序序列的元素都是在其最终位置上。
快速排序
思路
快速排序是基于分治法进行的。在待排序列L[1…n]中建立一个元素pivot作为枢轴(常为首元素)。通过一趟排序将序列分为独立的两个部分L[1…K-1]和L[K+1…n]。使L[1…K-1]中所有元素小于pivot,L[K+1…n]中所有元素大于等于pivot,pivot放在其最终位置L[K]上。然后递归地进行多次快速排序,直到所有子表只有一个元素或者为空,则所有元素都放在最终位置。
具体的一趟排序:
low和high指针分别指向首尾元素,pivot取首元素。
从high向左,找到第一个小于pivot的元素,将其替换到low处。
从low向右,找到第一个大于等于pivot的元素,将其替换到high处。
high和low交替向中间搜索元素,直到low==high,此处就是pivot的最终位置
若序列只有一个元素,显然已经有序,该元素就在最终位置处。
具体的递归机制可以看我的这篇帖子:【上机代码】函数调用栈,快速排序与归并排序.
代码
//partition实现从low和high向中间扫描序列,以low指向的元素为基准,将序列分为两个部分,基准在中间,左边所有元素小于基准,右边所有元素大于等于基准。
//返回基准值,和排序好的序列
int partition(int A[], int low, int high)
{
int pivot = A[low];
while (low < high) {
while (low < high && A[high] >= pivot)
--high;
A[low] = A[high];
while (low < high && A[low] <= pivot)
++low;
A[high] = A[low];
}
A[low]= pivot;
return low;
}
//基于递归实现,先按基准排序,然后分别递归处理基准左右的子表,子表长度>1时就继续划分,直到所有子表被处理完毕。
void QuickSort(int A[], int low, int high) {
if (low < high) {
int pivotpos = partition(A, low, high);
QuickSort(A, low, pivotpos - 1);
QuickSort(A, pivotpos + 1, high);
}
}
算法效率
1,空间复杂度
由于快速排序是递归的,占用的空间与递归的最大调用深度一致。为
O
(
l
o
g
n
)
O(log^{n})
O(logn)
2,时间复杂度
快速排序的运行速度与序列划分是否对称有关。
最坏情况,长度为n的序列,被分为n-1长度的子表和0长度子表。若每层递归都是这种情况(序列有序或逆序),对应的时间复杂度为
O
(
n
2
)
O(n^{2})
O(n2)
平均来说时间复杂度为
O
(
n
l
o
g
n
)
O(nlog^{n})
O(nlogn),是所有排序算法中平均表现最好的。
提高时间效率:选取一个可以将序列平均划分的枢轴,可以取头尾和中间元素的中间值为枢纽,或者取随机值为枢轴。
3,稳定性,不稳定
若在右子表有两个相同的元素,在交换到左子表后,相对次序会变化。
3 2 2‘
2’ 3 2
2‘ 2 3
4,排序后的位置
每次排序后都会将枢轴放在其最终位置上。
简单选择排序
1,思路
选择排序算法通过选择和交换来实现排序,其排序流程如下:
(1)序列表为L[1…n],每趟(第i趟)在L[i…n]中的n-i+1个(第一趟n个、第二趟n-1个…)待排元素中选取最小的元素,然后与L[i]交换。每趟确定一个元素的最终位置。
(2)然后不断重复,直到n-1趟,待排序列只剩一个元素就不需要再选择了。便完成了对原始序列的排序。
2,代码
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
//简单选择排序
void SelectSort(int A[], int n) {
for (int i = 0; i < n - 1; ++i) {
int min = i;
for (int j = i + 1; j < n; ++j) {
if (A[j] < A[min])
min = j;
}
if (min != i)
swap(A[i], A[min]);
}
}
链表实现
思路
关于链表的实现有两点疑问:
1,带头结点的单链表如何交换两个元素
A,B两个元素要设4个指针分别指向A、B的前后节点。交换时分相邻和不相邻两种情况:
(1)相邻只需要更改3个节点指向
A的前置指向B,B指向A,A指向B的后置
(2)不相邻要更改4个节点指向
A前置 A A后置 。。。。B前置 B B后置
A的前置指向B,B指向A的后置
B的前置指向A,A指向B的后置
2,简单选择排序中,链表如何区分两个元素
在数组的排序中,值是用下标来标识不同的元素,即使值相同也可区分。
在链表的排序中,在之前的遍历中已经找到的是<min的元素,找到的元素一定和min的值不相同,(相同的元素就不动了)故可以用p->data != min->data作为条件。或者结构体中加一个id来区分不同的元素。
代码
//简单选择排序(不带头结点,升序)
void select_sort(Listnode*& L)
{
//h为待排序列
Listnode* h = L, * p, * q, * prem, * m;
L = NULL;//L为已排序列
while (h!=NULL)
{
p = m= h;//m为最大值,p为待排元素
q = prem= NULL;//最大值前驱,q为p前驱
while (p!= NULL)//遍历序列找到最大值
{
if (p->data > m->data)
{
m= p;
prem= q;
}
q = p;
p = p->next;
}
if (m == h)//最大值在待排序列第一个,此时pmax为null
h = h->next;//易错
else
prem->next = m->next;//取出max
m->next = L;
L = m;
}
}
3,算法效率
⑴空间复杂度
仅使用常数个辅助单元,空间复杂度为O(1)
⑵时间复杂度
元素的移动次数较少,每趟最多移动3次,也就是说最多移动3(n-1)次
元素的比较次数固定,与初始序列无关,始终为n(n-1)/2次。
则时间复杂度为O(
n
2
n^{2}
n2)
⑶稳定性
不稳定,在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其他含有相同关键词的元素相对位置发生改变。
2 2‘ 1
1 2’ 2(破坏稳定性)
堆排序
1,堆
⑴概念
大根堆,L(i)>=L(2i)且L(i)>=L(2i+1)
小根堆,L(i)<=L(2i)且L(i)<=L(2i+1)
⑵堆与完全二叉树
可以将堆视为一棵完全二叉树,大根堆就是根节点为最大元素,且任何一个子树中根节点值大于等于左右孩子节点的值。小根堆则想反,根节点最小,任意子树中根节点值小于等于左右孩子节点的值。
⑶性质
根据完全二叉树的性质,可以推广得堆的性质。若节点按从上到下,从左到右依次编号。对于节点i。
①左孩子:2i
②右孩子:2i+1
③父节点:⌊i/2⌋
证明:
(1)证:完全二叉树中任何一层最左的节点编号n,则其左子树为2n,右子树为2n+1.
①显然,对于第L层的最左节点,等于L-1层最后一个节点编号+1;也就是从第1层到L-1层所有节点数+1。
②则:前L-1层节点数=
2
0
+
2
1
+
.
.
.
+
2
(
L
−
2
)
=
2
(
L
−
1
)
−
1
2^0+2^1+...+2^{(L-2)} = 2^{(L-1)}-1
20+21+...+2(L−2)=2(L−1)−1个(第i层有
2
(
i
−
1
)
2^{(i-1)}
2(i−1)个节点)。
③则第L层最左节点编号为
2
(
L
−
1
)
2^{(L-1)}
2(L−1),其左子树为第L+1层的最左节点,编号为
2
L
2^L
2L,右子树节点编号为
2
L
+
1
2^L+1
2L+1。得证。
(2)证:完全二叉树中任一节点编号n,则其左子树为2n,右子树为2n+1.
①取第L层的任意节点N,编号为n。设L层最左节点为M,编号为m。
②显然,L层中,N左边有n-m个节点。由于是完全二叉树,这n-m个节点都有左右孩子。
③则在L+1层中,N的左孩子NL左边有2(n-m)个节点。
④由(1)知第L+1层的最左节点编号为2m,则NL的编号为2m+2(n-m)=2n.得证
④i节点所在的层⌈ l o g i + 1 log^{i+1} logi+1⌉、⌊ l o g i log^{i} logi⌋+1
理解:
(每层最后一个元素编号=
2
x
−
1
2^{x}-1
2x−1,x为层号。则x=⌈
l
o
g
i
+
1
log^{i+1}
logi+1⌉=⌊
l
o
g
i
log^{i}
logi⌋+1,{2,对前者的理解,最大的也就是i取该层最后一个元素时,
l
o
g
i
+
1
log^{i+1}
logi+1=层号,再取该层其他元素值只会小于层号,故向上取整。2,对后者的理解,
l
o
g
i
log^{i}
logi取最小的也就是i取该层第一个元素时,
l
o
g
i
log^{i}
logi=层号-1,再取该层其他元素值只会大于层号-1,故向下取整。})
⑤若i为叶子结点:i>⌊n/2⌋(n为节点总数)
⑥若i为非叶子结点:i<=⌊n/2⌋
理解:
⌊n/2⌋就是最后一层最后一个元素的父节点,自然可以以它为界划分叶子和非叶子结点。
2,构建初始堆
⑴思路
n个元素的序列对应着n个节点的完全二叉树。其中最后一个节点的父节点为⌊n/2⌋,对⌊n/2⌋节点进行调整(若根节点小于左右孩子节点的较大者,则与较大者进行交换),使其构成大根堆。
循环对所有非叶子节点(⌊n/2⌋-1)进行上述调整,交换后可能破坏下一级,需要继续下沉对下一级子树进行调整。直到底或者下级子树满足堆条件。
循环到根节点,堆构建完毕。
⑵代码
//从序列k位置元素向下,调整为堆
void HeadAdjust(int A[], int k, int n) {
A[0] = A[k];//暂存根节点
for (int i = 2 * k; i <= n; i = i * 2)//下沉节点,沿左右孩子较大者
{
if (i < n && A[i] < A[i + 1])//找出左右孩子最大者
++i;
if (A[0] >= A[i])//大于等于孩子节点,调整完毕
break;
else {//小于孩子节点,交换
A[k] = A[i];
k = i;//根节点下沉到较大的孩子节点位置,继续循环对比,值仍然存在A[0]
}
}
A[k] = A[0];//根节点的值不断下沉,最后存于最终位置
}
//构建堆
void BuildMaxHeap(int A[], int n)
{
for (int i = n / 2; i > 0; i--)//遍历所有非叶子节点
HeadAdjust(A, i, n);
}
3,堆排序
⑴思路
①将L[1…n]中的n个元素构成初始堆(大根堆),则堆顶元素为最大值,输出堆顶元素后
②将堆底元素送入堆顶,此时根节点不满足堆的性质。将堆顶元素向下调整,直至满足堆的性质。然后输出堆顶元素
③重复,直到堆中只剩下一个元素。
⑵代码
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
//堆排序
void HeapSort(int A[], int n)//n为待排序列长度,非数组长度
{
BuildMaxHeap(A, n);//建立堆
for (int i = n; i > 1; i--) {//从最后一个节点开始向前遍历
swap(A[i], A[1]);//堆顶与堆底元素交换,存入有序序列最终位置
HeadAdjust(A, 1, i-1);//有序序列不再参与调整,继续调整堆。
}
}
⑶小根堆排序
只需改动堆调整部分,改为调整为小根堆即可。
void HeadAdjust(int A[], int k, int n) {
A[0] = A[k];
for (int i = 2 * k; i <= n; i = i * 2)
{
if (i < n && A[i] > A[i + 1])//找左右子树中较小的
++i;
if (A[0] <= A[i])//根节点小于等于左右子树就结束调整
break;
else {//根节点比左右子树小就交换
A[k] = A[i];
k = i;
}
}
A[k] = A[0];
}
4,插入删除
插入
思路,待插入元素先插入堆底。然后与父节点对比,若比父节点小就交换。然后再与新的父节点对比。循环直到比父节点大或者到根节点。(逆向调整)
注意: n为堆的元素数量,插入后要增加,因此用引用型
void Insert(int A[], int& n, int a) {
int i = ++n;
A[i] = a;
while (i / 2 > 0) {
if (A[i] < A[i / 2])
swap(A[i], A[i / 2]);
else//调整完毕 无需继续对比
break;
i = i / 2;
}
}
删除
思路,将堆底元素替换到待删元素处,序列长度减1。然后对该元素进行一次堆调整。
注意: n为堆的元素数量,删除后要减少,因此用引用型
void Delete(int A[], int& n, int i) {
A[i] = A[n];
--n;
HeadAdjust(A, i, n);
}
5,算法效率
⑴空间复杂度
常数级O(1)
⑵时间复杂度
堆排序主要分为建堆和调整堆两个步骤,下面分别进行分析:
void BuildMaxHeap( ) //建堆的时间复杂度为0(n)
①第i层最多有
2
i
−
1
2^{i-1}
2i−1个节点
②建立一个大根堆,最多需要从第1层对比到第h-1层,
∑
i
=
h
−
1
1
2
i
−
1
∗
(
h
−
i
)
∗
2
<
=
4
n
\sum_{i=h-1}^{1}{2^{i-1}*(h-i)*2}<=4n
∑i=h−112i−1∗(h−i)∗2<=4n
则建堆时间复杂度为0(n)
void HeadAdjust( ) //调整的时间复杂度为O(
n
l
o
g
2
n
nlog_{2}^{n}
nlog2n)
①一个节点在一次下沉中最多对比两次(左右孩子之间对比、孩子较大/小者与根对比)
②完全二叉树高为h,对第i层节点,下面最多有h-i层,则最多对比2(h-i),时间复杂度为O(h);又
h
=
⌊
l
o
g
2
n
⌋
+
1
h=⌊log^{n}_{2}⌋+1
h=⌊log2n⌋+1(每层最每层节点带入log+1都是>=
h
i
h_{i}
hi,且<
h
i
+
1
h_{i+1}
hi+1,所以向下取整。)
O(h)=O(
l
o
g
2
n
log_{2}^{n}
log2n)
③排序时最多对n-1个节点(从n到2)进行调整,则时间复杂度为O(
n
l
o
g
2
n
nlog_{2}^{n}
nlog2n)
则最终的时间复杂度为0(n+ n l o g 2 n nlog_{2}^{n} nlog2n)=O( n l o g 2 n nlog_{2}^{n} nlog2n)
⑶稳定性
不稳定,可能将后面相同值的节点调整到前面;
原始:1 2 2’
建堆:2 1 2’
排序:1 2’ 2
6,一些问题
⑴堆排序输入的len为序列长度
在上机代码中应该注意:
1,数组长度不等于序列长度,A[0]专门空出来用来暂存数据,因为在下沉的时候不方便处理。
2,堆排序几个函数中输入的length都是待排序的序列长度。在设计代码要注意创建的数组长度与输入排序函数的长度;比如排序后的输出要从1开始,而不是从0开始,到length结束。插入、删除要用引用型,以便修改序列长度。
⑵只排前几个元素用什么代码
比如序列有几百万个元素,只需要对前100个做出排序。
插入、快排、归并等排序都只有在全部排序后才得到最终位置。效率不高。
冒泡、堆排序和简单选择排序,每趟都是确定一个元素的最终位置。可以应用在这里。
归并排序
思路
1,总体思路
将两个或两个以上的有序序列两两合并为新的有序表。
初始将序列中n个元素视为n个长度为1的有序表,执行两两合并为⌈n/2⌉个长度为2或1的有序表。然后继续归并,直到合并为一个长度为n的有序序列。
2,如何归并
Merge(int A[],int low,int mid,int high)执行两两归并
两段有序序列为A[low,…,mid]和A[mid+,…,high],将两段序列复制到辅助数组B中,对数组B的两段序列,分别依次从头到尾取元素对比,较小者放入A中(从A[low]开始放,即覆盖原序列);若一段序列元素取完,则把剩下的元素都放入A中。
3,递归调用
MergeSort()
对序列进行递归调用,不满足low < high就会结束递归,然后从长度为1的相邻序列开始排序。最后对左右有序子表归并为长度为n的序列。
代码
int* B = (int*)malloc(10 * sizeof(int));//Merge中是和原始序列下标一样调用,不能按需分配。
void Merge(int A[], int low, int mid, int high) {
int i, j, k;
for (k = low; k <= high; k++)
B[k] = A[k];
for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
if (B[i] <= B[j])
A[k] = B[i++];
else
A[k] = B[j++];
}
while (i <= mid) A[k++] = B[i++];
while (j <= high) A[k++] = B[j++];
}
void MergeSort(int A[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort(A, low, mid);
MergeSort(A, mid + 1, high);
Merge(A, low, mid, high);
}
}
算法效率
空间复杂度
O(n) 辅助数组的长度n
时间复杂度
O( n l o g n nlog^{n} nlogn)
稳定性
稳定
2 1 1‘ 2’
先把左右子表内部有序
1 2 1‘ 2’
左==右时,先放左边的,保证有序
1 1‘ 2 2’
与直接插入排序(链表)
都是新建立一个辅助数据结构,然后在辅助结构中进行对比,找出最小的往原结构中填充。
1,归并排序是建立一个辅助数组B,在B中先比较各有序序列元素,按顺序覆盖进原数组A中
2,直接插入排序是,建立一个辅助链表h,同时将原链表清空。然后在辅助链表中依次找出最小的,插入原链表中。
基数排序(会手算即可)
思路
基于关键字各位大小排序。假设线性表中每个节点
a
j
a_{j}
aj由d元组{
k
j
d
−
1
,
k
j
d
−
2
,
.
.
.
,
k
j
1
,
k
j
0
k_{j}^{d-1},k_{j}^{d-2},...,k_{j}^{1},k_{j}^{0}
kjd−1,kjd−2,...,kj1,kj0}组成。
k
j
d
−
1
k_{j}^{d-1}
kjd−1为最主位关键字,
k
j
0
k_{j}^{0}
kj0为最次为关键字。通常排序方法可分为最高位优先和最低位优先。
最低位优先:
①以r为基数进行排序,r是各位取值的范围。建立队列
Q
0
.
.
.
.
Q
r
−
1
Q_{0}....Q_{r-1}
Q0....Qr−1
②分配:队列都置空,观测各个关键字的位
k
j
i
k_{j}^{i}
kji,位等于多少就放把该关键字入对应的队列中,如
k
j
i
=
k
k_{j}^{i}=k
kji=k,就放入
Q
k
Q_{k}
Qk
③收集:依次把队列
Q
0
.
.
.
.
Q
r
−
1
Q_{0}....Q_{r-1}
Q0....Qr−1中关键字首尾相连,就组成了新的元素。
④依次从低位到高位执行分配和收集。排序结束。
最高位优先:
从最高位向最低位依次执行分配和收集。
算法效率
空间复杂度
需要r个队列(r个头尾指针),则空间复杂度为O®
时间复杂度
d为位数,r为基数,n为序列长度
需要d趟分配和收集,每趟分配对序列遍历O(n),收集对队列遍历O®,则时间复杂度为O(d(n+r)),为固定值,与初始序列无关
稳定性
稳定,按位遍历就是保证了稳定性,同一位中值相同则右边入队;收集时则是反方向,使左边先出队,则最终还是保持了相对位置。
1 2 2‘ 3
Q1 Q2 Q3
1 2 3
2’
1 2 2’ 3
总结
时间复杂度
1,平均情况下
直接插入、折半插入、简单选择和冒泡排序为O(
n
2
n^{2}
n2)
快速排序、归并排序和堆排序为O(
n
l
o
g
n
nlog^{n}
nlogn)
基数排序为O(d(n+r)),d为位数,r为基数,n为序列长度
2,最好情况下
直接插入和冒泡排序可达O(n)
3,与初始序列无关
简单选择排序,基数排序
空间复杂度
1,快速排序,需要使用递归栈,平均复杂度为O(
l
o
g
n
log^{n}
logn)
2,2路归并排序,需要使用辅助数组,平均复杂度为O(n)
3,基数排序需要r个队列(r个头尾指针),则空间复杂度为O®
4,其他的都是O(1)
ps:注意简单选择排序的链表实现,虽然也新建了链表,但是只建立了指针,所以还是常数级。
稳定性
1,希尔排序,当值相同的值,划分到不同子表后,可能导致相对顺序变化。
2,快速排序,若在右子表有两个相同的元素,在交换到左子表后,相对次序会变化。
3,简单选择排序,在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其他含有相同关键词的元素相对位置发生改变。
4,堆排序,孩子和根交换,在序列中孩子和根之间隔着若干元素,可能会调到前面值相同元素之前,导致相对顺序变化。
5,其他排序是稳定的。
其他问题
1,每趟排序都能使一些元素放在最终位置:冒泡排序、快速排序、简单选择排序、堆排序
2,排序躺数与序列有关:交换类,冒泡,快排
3,关键字比较次数与序列无关,简单选择、折半插入
4,序列元素基本有序,适合冒泡、直接插入
5,序列较长,适合时间复杂度为O(
n
l
o
g
n
nlog^{n}
nlogn)的算法,也就是快速排序、归并排序和堆排序。其中快排适合元素随机分布情况;堆排序占用辅助空间较少,且最坏情况下时间复杂度也较小。前两者都不稳定,只有归并排序则是稳定的。
6,n很大,但是关键字位数小且可以分解是,基数排序效果不错
比如,n=10000,关键词为3位数字,则r为9,排3轮
基数排序:0(d(n+r))=30027
快速排序:0(nlog^{n})=132877
7,记录的信息比较大可以利用链表存储,减少移动时间(直接插入、冒泡、简单选择)