冒泡排序:
public
void
BubbleSort(
int
[]
x
)
{
for
(
int
i
=1;
i
<
x
.
length
;
i
++)
//若数组长度为n,则执行n-1次“冒泡”即可(每次可找出剩余元素的最大值,剩下的一个为最小值)
{
for
(
int
j
=0;
j
<
x
.
length
-
i
;
j
++)
//执行第i次“冒泡”的时候在数组尾部已经有i-1个排好序的“最大值”
{
if
(
x
[
j
]>
x
[
j
+1])
swap(
x
,
j
,
j
+1);
}
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
快速排序:
算法流程举例说明:
[4,5,3,9,10,1],首次执行:start=0, end=5;使用首个元素4做标兵,小于4的放在最左边,大于4的放在最右边,最后[1,3,4,9,10,5],返回mid为4的索引2;
然后对start=0,end=2-1=1递归;
然后对start=2+1=3,end=5递归;
public
void
QuickSort(
int
[]
x
,
int
start
,
int
end
)
{
if
(
start
>=
end
)
return
;
int
mid
=getMiddleIndex(
x
,
start
,
end
);
//mid左面的部分都小于等于x[mid],mid右面的部分都大于等于x[mid]
QuickSort(
x
,
start
,
mid
-1);
QuickSort(
x
,
mid
+1,
end
);
}
public
int
getMiddleIndex(
int
[]
x
,
int
start
,
int
end
)
{
int
t
=
x
[
start
];
//选当前无序部分的第一个元素做标兵
while
(
start
<
end
)
{
while
(
start
<
end
&&
x
[
end
]>=
t
)
end
--;
x
[
start
]=
x
[
end
];
//x[end]是本次从右至左遍历找到的第一个小于t的值
while
(
start
<
end
&&
x
[
start
]<=
t
)
start
++;
x
[
end
]=
x
[
start
];
//x[start]是本次从左至右遍历找到的第一个大于t的值
}
x
[
start
]=
t
;
//循环结束后,对应本次函数处理的部分数组(假设索引从a到b),a到start-1为小于等于t的部分,start+1到b为大于等于
t的部分
return
start
;
//此时start=end
}
void QuickSort(vector<int> data, int start,int end){
if(start==end) return;
int index=Partition(data,start,end);
if(index>start)
QuickSort(data,start,index-1);
if(index<end)
QuickSort(data,index+1,end);
}
int Partition(vector<int> data, int start, int end){
if(data==NULL || start<0 || end>=data.size())
return 0;
int index=start+rand()%(end-start+1);
swap(&data[index],&data[end]);
int small=start-1;
for(index=start;index<end;++index){
if(data[index]<data[end]){
++small;
if(small!=index)
swap(&data[index],&data[small]);
}
}
++small;
swap(&data[small],&data[end]);
return small;
}
时间复杂度:O(n*logn)
空间复杂度:O(n*logn)
稳定性:不稳定
简单选择排序:
算法流程:每次找到未排序部分的最小值的位置,和未排序部分的第一个元素交换位置。虽然交换了,但还是选择排序!!!
public
void
SimpleSelectSort(
int
[]
x
)
{
for
(
int
i
=0;
i
<
x
.
length
-1;
i
++)
//每次找到当前数组剩余元素中的最小值,和剩余部分元素的首元素交换。五个元素交换四次即可。
{
//i指向当前未排序部分(即剩余部分)的首元素
int
min
=
x
[
i
];
//先假定x[i]是x[i]到x[x.length-1]的最小值
int
index
=
i
;
for
(
int
j
=
i
+1;
j
<
x
.
length
;
j
++){
//从i+1开始直到数组最后找到最小值和它的index
if
(
x
[
j
]<
min
){
min
=
x
[
j
];
index
=
j
;
}
}
swap(
x
,
i
,
index
);
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定 ([2,2,1],循环首次执行之后1与首元素,即第一个2交换,变成[1,2,2]。第一个二跑到了最后,在原来第二个2之后)
堆排序:
最大堆:父结点的值大于等于任意一个子节点的值;
最小堆:父结点的值小于等于任意一个子节点的值;
public
void
HeapSort(
int
[]
x
){
//每次将0到i的元素建堆(只是把数组想作堆结构)后,找到堆顶的元素(即x[0])与i位置的元素交换。此时0到i-1为当前无序部分,i及之后都是有序部分。
for
(
int
i
=
x
.
length
-1;
i
>=1;
i
--){
//使堆中所有的子节点都小于等于它的父结点
BuildHeap(
x
,
i
);
swap(
x
,0,
i
);
}
}
public
void
BuildHeap(
int
[]
x
,
int
LastIndex
){
//从LastIndex到1位置的元素(不用管0位置的元素,因为它没有父结点,它是根)分别找到它们的父结点,如果父结点的值比它的全部子节点的值都大,则不变;否则将父结点的值与子节点中的较大值互换(可能存在当前父结点没有右子节点的情况)
for
(
int
i
=
LastIndex
;
i
>=1;
i
--){
int
curParentNode
=(
i
-1)/2;
//(当前结点下标-1)/2即为它的父结点的下标,无论左右结点都适用
int
biggerSonNode
=-1;
if
(2*
curParentNode
+2>
LastIndex
)
//如果当前检测结点的父结点没有右子节点
biggerSonNode
=
i
;
else
biggerSonNode
=
x
[
curParentNode
*2+1]>=
x
[
curParentNode
*2+2]?(
curParentNode
*2+1):(
curParentNode
*2+2);
if
(
x
[
curParentNode
]<
x
[
biggerSonNode
])
swap(
x
,
curParentNode
,
biggerSonNode
);
}
}
void HeapSort(vector<int> num){
if(num.size()<=0) return;
for(int i=num.size()-1;i>=1;--i){
BuildHeap(num,i);
swap(num[0],num[i]);
}
}
void BuildHeap(vector<int> num, int Last){
for(int i=last;i>=1;--i){
int Parent=(i-1)/2;
int biggerSon=-1;
if(2*Parent+2>Last)
biggerSon=i;
else biggerSon=num[Parent*2+1]>=num[Parent*2+2]?Parent*2+1:Parent*2+2;
if(num[Parent]<num[biggerSon])
swap(num[Parent],num[biggerSon]);
}
}
时间复杂度:O(n*logn)
空间复杂度:O(1)
稳定性:不稳定
直接插入排序:
void StraightInsertSort(vector<int> num){
for(int i=1;i<num.size();++i){
int cur=num[i];
int j=i-1;
for(;j>=0 && num[j]>cur;j--)
num[j+1]=num[j];
num[j+1]=cur;
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
希尔排序:(最小增量排序)
算法流程举例说明:
[5,1,3,7,23,72,7],gap为3;故分为三组:5,7,7;1,23;3,72;先分别对三组数据进行直接插入排序。数组变成[5,1,3,7,23,72,7]。然后gap变为1,将数组分为一组,即本身,然后对这一组进行直接插入排序。
void ShellSort(vector<int> num){
int gap=num.size()/2;
while(gap>=1){
//最外层循环控制步长
for(int i=0;i<gap;++i){
for(int j=i+gap;j<=num.size();j+=gap){
//对每一组子序列进行单独进行直接插入排序
int cur=num[j];
int index=j-gap;
for(;index>=0 && num[index]>cur;index-=gap){
num[index+gap]=num[index];
}
num[index+gap]=cur;
}
}
gap/=2;
}
}
时间复杂度:O(n^1.25)到O((1.6n)^1.25)(经验公式)
空间复杂度:O(1)
稳定性:不稳定
基数排序:(使用了桶排序原理)
算法流程举例说明:
//x中最初存储[5,1,3,7,23,72,7].确认最大位数为十位,故只需整体入桶两次即可(先按照个位后按照十位)。
//首先按照个位入桶
//桶1:1
//桶2:72
//桶3:3,23
//桶5:5
//桶7:7,7
//其余的桶为空
//按桶从小到大,同一个桶从左到右的顺序将全部元素放回数组x中(由于使用的是队列,每次头元素入x的同时就会清空桶(队列)中的头元素)
//x为[1,72,3,23,5,7,7],再按照十位入桶
//桶0:1,3,5,7,7
//桶2:23
//桶7:72
//其余的桶为空
//按桶从小到大,同一个桶从左到右的顺序将全部元素放回数组x中
//x为[1,3,5,7,7,23,72],完毕
public
void
RadixSort(
int
[]
x
)
{
int
max
=
x
[0];
//找到数组中的最大值
for
(
int
i
:
x
)
{
if
(
i
>
max
)
max
=
i
;
}
int
maxbit
=0;
//计算这个最大值对应十进制有多少位
while
(
max
!=0)
{
max
/=10;
maxbit
++;
}
int
dividend
=1;
//构建十个桶,每个桶用LinkedList实现
List<LinkedList<Integer>>
list
=
new
ArrayList<>();
for
(
int
i
=0;
i
<10;
i
++)
list
.add(
new
LinkedList<Integer>());
//从个位到高位处理
for
(
int
i
=0;
i
<
maxbit
;
i
++)
{
for
(
int
j
=0;
j
<
x
.
length
;
j
++)
list
.get((
x
[
j
]/
dividend
)%10).offer(
x
[
j
]);
//将每次将桶号从小到大,每个桶中从头到尾的顺序,将排好的数放回到x数组中
int
m
=0;
for
(
int
j
=0;
j
<
list
.size();
j
++)
{
while
(
list
.get(
j
).size()!=0)
x
[
m++
]=
list
.get(
j
).poll();
}
dividend
*=10;
}
}
int
maxbit(
int
data[],
int
n)
//辅助函数,求数据的最大位数
{
int
d = 1;
//保存最大的位数
int
p = 10;
for
(
int
i = 0; i < n; ++i)
{
while
(data[i] >= p)
{
p *= 10;
++d;
}
}
return
d;
}
void
radixsort(
int
data[],
int
n)
//基数排序
{
int
d = maxbit(data, n);
int
*tmp = new int[n];
int
*count = new int[10];
//计数器
int
i, j, k;
int
radix = 1;
for
(i = 1; i <= d; i++)
//进行d次排序
{
for
(j = 0; j < 10; j++)
count[j] = 0;
//每次分配前清空计数器
for
(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10;
//统计每个桶中的记录数
count[k]++;
}
for
(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j];
//将tmp中的位置依次分配给每个桶
for
(j = n - 1; j >= 0; j--)
//将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for
(j = 0; j < n; j++)
//将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete
[]tmp;
delete
[]count;
}
时间复杂度:O(d*n) d为长度(对应代码中maxbit),n为数组长度
空间复杂度:O(n)
稳定性:稳定
归并排序:
void MergeSort(vector<int> num, int start, int end){
//将当前待处理的部分继续划分为两部分,初始输入的start=1,end=x.length-1
if(start>=end) return;
int mid=start+(end-start)/2;
MergeSort(num,start,mid);
MergeSort(num,mid+1,end);
Merge(num,start,mid,mid+1,end);
//将划分好的两部分归并。每次归并的两个部分都已经是有序的了。
}
void Merge(vector<int> num, int start1, int end1, int start2, int end2){
vector<int> temp(end2-start1+1);
//临时生成一个新的数组用来存储
int tempIndex=0;
int first=start1;
int second=start2;
while(first<=end1 && second<=end2){
//直到遍历到某个部分的最后一个元素
if(num[first]<=num[second]){
/first和second始终指向当前第一(二)个部分剩余元素的最小值
temp[tempIndex]=num[first];
tempIndex++;
first++;
}
else{
temp[tempIndex]=num[second];
tempIndex++;
second++;
}
}
while(first<=end1)
//若第一个部分还有剩余没有并入新的临时数组,则直接并入
temp[tempIndex++]=num[first++];
while(second<=end2)
//若第二个部分还有剩余没有并入新的临时数组,则直接并入
temp[tempIndex++]=num[second++];
for(int i=0;i<tempIndex;++i)
//将并好的有序数组写入原始数组的相应位置(从第一部分的起始到第二部分的结束)
num[start1+i]=temp[i];
}
时间复杂度:O(n*logn)
空间复杂度:O(n):n+logn
稳定性:稳定
种类 | 名称 | 时间复杂度 | 空间复杂度 | 稳定性 |
交换排序 | 冒泡排序 | O(n^2) | O(1) | 稳定 |
快速排序 | O(n*logn) | O(n*logn) | 不稳定 | |
选择排序 | 简单选择排序 | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n*logn) | O(1) | 不稳定 | |
插入排序 | 直接插入排序 | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^1.25)到O((1.6n)^1.25) | O(1) | 不稳定 | |
归并排序 | 归并排序 | O(n*logn) | O(n) | 稳定 |
基数排序 | 基数排序 | O(d*n) | O(n) | 稳定 |
选择排序算法准则
每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n。
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序:如果内存空间允许且要求稳定性的,
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
2)当n较大,内存空间允许,且要求稳定性,推荐归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
5)一般不使用或不直接使用传统的冒泡排序。
6)基数排序
它是一种稳定的排序算法,但有一定的局限性:
1、关键字可分解。
2、记录的关键字位数较少,如果密集更好
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。