1.常见排序算法复杂度
2.冒泡排序
步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 重复步骤1~3,直到排序完成。
最快 :正序
最慢:逆序
public int[] sort(int[] arr){
for (int i = 1; i < arr.length; i++) {
// 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
boolean flag = true;
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = false;
}
}
if (flag) {
break;
}
}
return arr;
}
3.选择排序
步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
public int[] sort(int[] arr){
// 总共要经过 N-1 轮比较
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
// 每轮需要比较的次数 N-i
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
// 记录目前能找到的最小值元素的下标
min = j;
}
}
// 将找到的最小值和i位置所在的值进行交换
if (i != min) {
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
return arr;
}
4.插入排序
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
public int[] sort(int[] arr) {
// 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
for (int i = 1; i < arr.length; i++) {
// 记录要插入的数据
int tmp = arr[i];
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && tmp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
// 存在比其小的数,插入
if (j != i) {
arr[j] = tmp;
}
}
return arr;
}
5.希尔排序
它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个记录恰被分成一组,算法便终止。
选择增量gap=length/2,这是希尔建议的增量,但并不是最优。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
public void shellSort(int[] arr) {
int length = arr.length;
int temp;
for (int step = length / 2; step >= 1; step /= 2) {
for (int i = step; i < length; i++) {
temp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > temp) {
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = temp;
}
}
}
6.归并排序
-
先递归分解,把长度为n的输入序列分成两个长度为n/2的子序列,直至子序列数据个数为一,可以认为序列有序。然后合并。
-
申请空间,使其大小为n,该空间用来存放合并后的序列;
-
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置; -
重复步骤 3 直到某一指针达到序列尾;
-
将另一序列剩下的所有元素直接复制到合并序列尾。
-
重复上述步骤,直至合并为一个序列。
public void sort(int []arr){
int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr,0,arr.length-1,temp);
}
private void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
7.快速排序
1.从数列中挑出一个"基准"(pivot)元素,一般取数列第一个。
2.重新排序数列,所有比基准值小的元素放在基准前面,所有比基准值大的元素摆在基准的后面(相同的数可以到任一边)。一趟排序之后,该基准就处于数列的中间位置。
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
void quick_sort(int []arr,int low ,int high){
if(low < high){
int i=low,j=high,x=arr[low];
while(i<j){
while(i<j&&arr[j] >=x) --j;
if(i<j) arr[i++] = arr[j];
while(i<j&&arr[i] <=x) ++i;
if(i<j) arr[j--] = arr[i];
}
arr[i] = x;
quick_sort(arr,low,i-1);
quick_sort(arr,j+1,high);
}
}
非递归写法
// start和end为前闭后闭
private static void nonRec_quickSort(int[] a, int start, int end) {
// 用栈模拟
Stack<Integer> stack = new Stack<>();
if (start < end) {
stack.push(end);
stack.push(start);
while (!stack.isEmpty()) {
int l = stack.pop();
int r = stack.pop();
int index = partition(a, l, r);
if (l < index - 1) {
stack.push(index - 1);
stack.push(l);
}
if (r > index + 1) {
stack.push(r);
stack.push(index + 1);
}
}
}
System.out.println(Arrays.toString(a));
}
private static int partition(int[] a, int start, int end) {
int pivot = a[start];
while (start < end) {
while (start < end && a[end] >= pivot)
end--;
a[start] = a[end];
while (start < end && a[start] <= pivot)
start++;
a[end] = a[start];
}
a[start] = pivot;
return start;
}
算法分析:
快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。
1、最坏时间复杂度
最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。因此,快速排序必须做n-1次划分,第i次划分开始时区间长度为n-i+1,所需的比较次数为n-i(1≤i≤n-1),故总的比较次数达到最大值:Cmax= n(n-1)/2=O(n2)
2、最好时间复杂度
在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:0(nlgn)
注意:用递归树来分析最好情况下的比较次数更简单。因为每次划分后左、右子区间长度大致相等,故递归树的高度为O(lgn),而递归树每一层上各结点所对应的划分过程中所需要的关键字比较次数总和不超过n,故整个排序过程所需要的关键字比较总次数C(n)=O(nlgn)。
因为快速排序的记录移动次数不大于比较的次数,所以快速排序的最坏时间复杂度应为0(n2),最好时间复杂度为O(nlgn)。
3、空间复杂度
快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为O(lgn),故递归后需栈空间为O(lgn)。最坏情况下,递归树的高度为O(n),所需的栈空间为O(n)。
4、稳定性
快速排序是非稳定的。
参考:一篇就够 快速排序的原理及其java实现(递归与非递归)
8.堆排序
堆是一棵顺序存储的完全二叉树。
其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。
其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆。
举例来说,对于n个元素的序列{R0, R1, … , Rn}当且仅当满足下列关系之一时,称之为堆:
(1) Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)
(2) Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)
其中i=1,2,…,n/2向下取整;
9.计数排序
基础版算法步骤:
第一步:找出原数组中元素值最大的,记为max。
第二步:创建一个新数组count,其长度是max加1,其元素默认值都为0。
第三步:遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。
第四步:创建结果数组result,起始索引index。
第五步:遍历count数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result数组中去,每处理一次,count中的该元素值减1,直到该元素值不大于0,依次处理count中剩下的元素。
第六步:返回结果数组result。
public int[] countSort(int[] A) {
// 找出数组A中的最大值
int max = Integer.MIN_VALUE;
for (int num : A) {
max = Math.max(max, num);
}
// 初始化计数数组count
int[] count = new int[max+1];
// 对计数数组各元素赋值
for (int num : A) {
count[num]++;
}
// 创建结果数组
int[] result = new int[A.length];
// 创建结果数组的起始索引
int index = 0;
// 遍历计数数组,将计数数组的索引填充到结果数组中
for (int i=0; i<count.length; i++) {
while (count[i]>0) {
result[index++] = i;
count[i]--;
}
}
// 返回结果数组
return result;
}
基础版有浪费空间的缺陷,优化版 ,将数组长度定为max-min+1,即不仅要找出最大值,还要找出最小值,根据两者的差来确定计数数组的长度。
优化版参考:一文弄懂计数排序算法!
漫画:什么是计数排序?