排序算法稳定性的定义?
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
1.1冒泡排序的思想
对相邻的元素进行两两比较,顺序相反则进行交换。这样每一趟将最小或最大的元素浮到顶端。最终达到完全有序。
冒泡排序一般是将最大元素浮到顶端,数组按照从小到大排序,每一趟都将最大值浮到相对顶端,
因此内层循环就是 for(int j = 0; j<length-i-1;j++)
1.2常见问题
1.冒泡排序时间复杂度是?
冒泡排序最坏的时间复杂度是O(n^2)。
2.为什么最坏的时间复杂度是O(n^2)?
考虑最坏的情况,完全倒序,那么需要比较的次数就是n+(n-1)+(n-2)+....1,等差数列求和,O(n^2)
3.如何优化时间复杂度?
在排序过程中发现后面的元素已经是排好序了,那么后面的继续便利就是浪费了,优化的思想其实就是添加一个标志,若在某一趟比较中没有发生交换,则进行提前终止后面的比较,提高算法的效率。这样在(最佳)顺序排列的时候时间复杂度O(n),也就是检查一轮就结束排序。
/**
* 原因,当冒泡排序发现元素已经是排序好了的时候,并不会停止这将会浪费时间复杂度。
* 分析:当把大的数不断后移的时候,在下一次再次进行排序检验的时候发现当前已经是排好序的了,那么不再继续循环直接跳出
* 这里节省的时间复杂度值得是:减少了for循环的次数:比如{1, 3, 2 ,9 ,4} 经过
* 第一次冒泡后变为{1,2,3,4,9},此时的flag仍为false,然后继续下一波外层循环,flag此时为true了,但是内部for循环还是要走一遍
* 这个是必须的否则就会和第二种优化一样不够健壮,在内部for循环里面,没有一次可以进入if判断了,说明在已经排好的最大数的前面的数组已经为有序的了
* 此时才应该是break,这种优化只是部分上优化了冒泡。
*/
//外循环表示趟数,内循环表示该趟比较次数,每一次都将大的元素向上移动。
public class BubbleSortAnalyse {
public static int[] sort(int[] data) {
int temp;
boolean flag = false;
for (int i = 0; i < data.length - 1; i++) {
flag = true;
for (int j = 0; j < data.length - i - 1; j++) {
if (data[j] > data[j + 1]) {
temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
flag = false;
}
}
// 如果上一轮冒泡排序已经全部有序
// 即flag!=false,则直接退出,不用进行下一轮冒泡循环,提高效率,否则数组已经有序了,他还会继续冒泡循环
if (flag) {
break; // 可以注释这一行,单步测试或者查看i的值即可验证
}
// System.out.println(i);
}
return data;
}
// 冒泡排序优化,通过flag进行判断排序是否提前已经结束。
// 该优化存在缺陷,当最小的数可能还在后面已经排好序的数组中时就已经停掉了,
//
public static void BubbleSort(int[] arr) {
int n = arr.length;
int i, j, temp, flag; // temp临时变量,flag是否提前结束的标志位
flag = 1;// flag等于1表示循环没有结束,0表示循环已经结束
// 冒泡排序
for (i = 1; i < n && flag == 1; i++) {flag = 0;
for (j = 0; j < n - i; j++) {
// 如果以后的循环不改变flag的值(flag始终为0),说明没有发生数组交换
// 也就是说这个数组已经是排好序的了。
if (arr[j] > arr[j + 1]) {
// 只要是有一次也需要置为1
flag = 1;
// 交换
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}}
}
}
public static void main(String[] args) {
int[] arr = { 2, 3, 9, 1, 10 };
// BubbleSort(arr);
int[] arr2 = sort(arr);
for (int i : arr2) {
System.out.println(i);
}
}
}
4.冒泡排序是否稳定?
稳定,但是冒泡排序也可以转化为不稳定算法。
即 将比较的条件 if (a[i]>a[i+1]) ====> if (a[i]>=a[i+1]) 转化为不稳定排序算法。
2.快排
2.1快速排序的思想
1.选择一个基数(如何选择?)。
2.分区,把小于基数的排左边,大于基数的排右边。
3.对得到两个分区重复以上步骤,直至分区只有1个元素
2.2快排的时间和空间的复杂度?
1.当分区选取的基准元素为待排序元素中的最大或最小值时,为最坏的情况,时间复杂度和直接插入排序的一样,移动次数达到最大值
Cmax = 1+2+...+(n-1) = n*(n-1)/2 = O(n2) 此时时间复杂度为O(n2)
2.当分区选取的基准元素为待排序元素中的"中值",为最好的情况,时间复杂度为O(nlog2n)。
3.快速排序的空间复杂度为O(log2n).
4.当待排序元素类似[6,1,3,7,3]且基准元素为6时,经过分区,形成[1,3,3,6,7],两个3的相对位置发生了改变,
所是快速排序是一种不稳定排序。
快排最坏的情况是O(n^2),顺序或者逆序都是。最佳的情况就是O(nlogn)。
(注意:所谓最坏其实就是分治的时候分成两个极不平衡的数组,比如n个元素分成一个是n-1个元素和一个1个元素的.)
快排的空间复杂度最坏的情况下是O(n),通常情况下为O(logn)
2.3 快排的核心代码
其实快排的核心代码是下面这一部分:
while(i<j)
{
while(i<j && a[j]>=temp)
j--;
a[i]=a[j];
while(i<j && a[i]<=temp)
i++;
a[j]=a[i];
}
a[i]=temp;
2.4 快排递归完整代码
void sort(int a[],int left,int right)
{
if (left >= right)
{return; }
int i,j,temp;
i=left;
j=right;
temp=a[i];
while( i<j)
{
while(a[j]>=temp && i<j)
{
j--;
}
a[i]=a[j];
while(a[i]<=temp && i<j)
{
i++;
}
a[j]=a[i];
}
a[i]=temp;
sort(a,left,i-1);
sort(a,i+1,right);
}
Java实现
public class QuickSort { public static void quickSort(int arr[],int _left,int _right){ int left = _left; int right = _right; int temp = 0; if(left <= right){ //待排序的元素至少有两个的情况 temp = arr[left]; //待排序的第一个元素作为基准元素(可优化的点) while(left != right){ //从左右两边交替扫描,直到left = right while(right > left && arr[right] >= temp) right --; //从右往左扫描,找到第一个比基准元素小的元素 arr[left] = arr[right]; //找到这种元素arr[right]后与arr[left]交换 while(left < right && arr[left] <= temp) left ++; //从左往右扫描,找到第一个比基准元素大的元素 arr[right] = arr[left]; //找到这种元素arr[left]后,与arr[right]交换 } arr[right] = temp; //基准元素归位 quickSort(arr,_left,left-1); //对基准元素左边的元素进行递归排序 quickSort(arr, right+1,_right); //对基准元素右边的进行递归排序 } } public static void main(String[] args) { int array[] = {10,5,3,1,7,2,8}; System.out.println("排序之前:"); for(int element : array){ System.out.print(element+" "); } quickSort(array,0,array.length-1); System.out.println("\n排序之后:"); for(int element : array){ System.out.print(element+" "); } } }
2.5 快排非递归完整代码
/**把数组分为两部分,轴pivot左边的部分都小于轴右边的部分**/
template
<
typename
Comparable>
int
partition(vector<Comparable> &vec,
int
low,
int
high){
Comparable pivot=vec[low];
//任选元素作为轴,这里选首元素
while
(low<high){
while
(low<high && vec[high]>=pivot)
high--;
vec[low]=vec[high];
while
(low<high && vec[low]<=pivot)
low++;
vec[high]=vec[low];
}
//此时low==high
vec[low]=pivot;
//找到最终存放基数的索引
return
low;
}
/**使用栈的非递归快速排序**/
template
<
typename
Comparable>
void
quicksort2(vector<Comparable> &vec,
int
low,
int
high){
stack<
int
> st;
if
(low<high){
int
mid=partition(vec,low,high);//找到基数的存放索引位置,左边小,右边大
if
(low<mid-1){
st.push(low);
st.push(mid-1);
}
if
(mid+1<high){
st.push(mid+1);
st.push(high);
}
//其实就是用栈保存每一个待排序子串的首尾元素下标,
//下一次while循环时取出这个范围,对这段子序列进行partition操作
while
(!st.empty()){
int
q=st.top();
st.pop();
int
p=st.top();
st.pop();
mid=partition(vec,p,q);
if
(p<mid-1){
st.push(p);
st.push(mid-1);
}
if
(mid+1<q){
st.push(mid+1);
st.push(q);
}
}
}
}
说白了,非递归就是搞一个栈将我们用partition函数分出来的左右区间首尾位存到栈中,然后进行while循环。
2.6 如何优化快排?
优化思路:基数的选择
方法一:不要总把数组第一个数选择基数,采取随机选择。
方法二:三数取中。比方说有序列: 8 1 4 9 6 3 5 2 7 0
取最左边、最最右边以及中间的。分别是8 0 6。取三个数中间的数即 0 6 8 的6。
把取到的数和序列第一个数交换,也就是得到序列: 6 1 4 9 8 3 5 2 7 0,继续进行快排。
其实方法一、方法二都是针对基数的选择来进行优化,避免分治的时候分成两个极不平衡的数组。
3.归并排序(分治)
3.1 归并排序原理
具体可以看看博客:
https://www.cnblogs.com/chengxiao/p/6194356.html
原理大概是两个阶段:
阶段1是分。分的时候就是把整个数组分成只有一个元素的数组。
阶段2是合。就是两两合并,将两个已经有序的子序列合并成一个有序序列。合并的时候利用两个哨兵i,j。分别指向两个集合。然后比较-移动,看看核心代码就明白。
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。(上例 log2 8 即3)
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
3.2 归并时间与空间复杂度
归并排序的时间复杂度为O(nlogn),最坏也是O(nlogn).是一种稳定的算法
归并排序的空间复杂为O(n),这里主要是由在merge时候产生一个O(n)的辅助数组temp决定的.
3.3 归并排序核心代码
归并排序的核心代码。
首先是要想到两个集合的哨兵应该是:
int i=low;
int j=mid+1;
之后就是比较两个集合哨兵位置的元素大小
if (list[i]>=list[j])
{
temp[k++]=list[j++];
}
else
{
temp[k++]=list[i++];
}
再补上循环的条件,循环条件就应该是两个集合的最末端。
while (i<=mid && j<=high)
{
if (list[i]>=list[j])
{
temp[k++]=list[j++];
}
else
{
temp[k++]=list[i++];
}
}
最后,肯定有一个集合有剩。就需要把元素都取完
while( i<=mid )
{
temp[k++]=list[i++];
}
while( j<=high)
{
temp[k++]=list[j++];
}
之后就把temp粘回原来的list中,开始结束的终点分别是low,high
for(i=low,k=0;i<=high;i++,k++)
{
list[i]=temp[k];
}
另外一部分核心代码就是分的阶段。很简单,不断递归:
void split(int list[],int left,int right)
{
if (left<right)
{
int mid=(left+right)/2;
split(list,left,mid);
split(list,mid+1,right);
merge(list,left,mid,right); // 调用治
}
Java实现
public class MergeSort { public static void main(String []args){ int []arr = {9,8,7,6,5,4,3,2,1}; sort(arr); System.out.println(Arrays.toString(arr)); } public static void sort(int []arr){ int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间 sort(arr,0,arr.length-1,temp); } //不断地递归切分 private static void sort(int[] arr,int left,int right,int []temp){ //只有一个元素时,停止递归,也就是left=right的时候 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 static 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++]; } } }
4.堆排序
推荐博客:堆排序
基本思想:初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆(一个for循环),将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,再次输出堆顶元素(一个for循环),得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。
时间复杂度分析:O(nlog(n)),堆排序是一种不稳定的排序算法。
因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建堆?
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆?
首先讨论第二个问题:输出堆顶元素后,怎样对剩余n-1元素重新建成堆?
调整小顶堆的方法:
1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。
2)将根结点与左、右子树中较小元素的进行交换。
3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).
4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).
5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
堆排其实就是升序的时候用大顶堆(最大值在根节点),降序用小顶堆(最小值在根节点)。
而大顶堆或者小顶堆其实是一个完全二叉树,只不过这颗完全二叉树还满足以下性质:
任意一个父节点的值大于或者等于其左右孩子节点的值。
即有:a[i]>=a[i*2+1] 且a[i]>=a[i*2+2]。(根节点的编码为0,如果编码为1的话就是a[i]>=a[i*2] 且a[i]>=a[i*2+1])
任意节点的左右子树也是大顶堆。对左右孩子节点的值没有任何要求。
这样的完全二叉树就是一个大顶堆(类似有小顶堆)
另外,完全二叉树可以变现为一个一维的数组。比如:
使用大顶堆来排序的步骤其实很简单,只需要两部分。
第一部分:建堆。
第二部分:把要排序的元素放到堆顶。
建堆:
1.首先将所有元素按照初始顺序填充到一个完全二叉树中
2.从“最后一个非叶子节点”开始,调用siftdown方法,调整堆的结构,直到根节点为止
降序的话,那就建立小顶堆;(父节点的值小于两个子节点的值)
升序的话,就建立大顶堆;(父节点的值大于两个子节点的值)
https://blog.csdn.net/jinyongqing/article/details/12651349
建堆思路:
第一步:
找到第一个非叶子节点。这个非叶子节点的编号为len/2-1。
len是待排序列的长度,而且注意节点编码从0开始,也就是说根节点编号为0。如果根节点编号从1开始的化就是len/2。而且这里取整的。)
比如,上面图的第一个非叶子节点是(5/2-1)=(2-1)=1。编码为1的节点为第一个非叶子节点。
为啥是len/2-1呢。简单解释一下。
len是整个数组长度,也是完全二叉树里面编号最大的一个值。编号最大的这个值所在的这一层肯定是最后一层,其上一层就是非叶子节点所在的第一层。len/2就是上一层其中一个节点的编号。
第二步:
从第一个非叶子节点开始,比较其与左右孩子节点值的大小。如果父节点值小于孩子节点的值,那么需要把两者交换。
注意注意注意!!!交换之后,要对交换后要以被交换的孩子节点为根节点建立一个大顶堆,其实就是递归的过程。
第三步:
叶子节点的编号不断 -1 其实就是先左移到最左边,然后移到上一层的最右边。即从右->左。
最后可以建立好一个大顶堆。
注意注意注意!!这个大顶堆一定有任意的父节点值大于或者等于其左右孩子节点值。
上一部分把大顶堆建好之后就可以用大顶堆来排序了。
注意注意!!!上面提到大顶堆其实是一个一维数组,但是即使已经是大顶堆了,这个数组也是无序的。只不过这个数组的第一个元素是最大的!!!!!
所以,建好堆之后的排序每次其实就是筛选出次最大值!!!!!!!
具体做法:
每次把堆尾a[len-1]的元素移动到堆顶a[0]来,并且把堆的长度-1,然后重新建堆,但是这次建堆不是完全的重新建立,而是一个调整。
思路看完,还是得看代码,代码一步一步撸:
首先,我们核心是要比较父节点和左右孩子节点的大小:
if (a[max]<a[left] && left<=heapsize ){
max=left;
}
if (a[max]<a[right] && right<=heapsize){
max=right;
}
而左右孩子节点的编号由关系有:
//注意节点编码从0开始
int left =i*2+1;
int right=i*2+2;
int max=i;
max用来存放最大的节点。初始化成i就可以了。
然后,当遇到左右孩子节点比父节点大的时候需要进一步判断被更换后的节点是否保持大顶堆的性质,比如:
左边的树,左孩子节点1比父节点0要大,要更换,更换后,节点[1,3,4]不满足大顶堆的性质,是节点1导致的,需要把节点1看做作根节点进行一次建大顶堆,具体来说代码:
//也就是说,max在上面被重新赋值了,也就是根节点和左右子节点比较后需要进行互换
if (i!=max)
{
int temp=0;
temp=a[i];
a[i]=a[max];
a[max]=temp;
BUildheap(a,max,heapsize);
}
if用来判断是否父节点小于左(右)孩子节点的值。然后调用Buildheap,所以完整代码:
void Buildheap(int a[],int i,int heapsize):
{
int left =i*2+1;
int right=i*2+2;
int max=i ;
if (left<heapsize && a[max]<a[left] ){
max=left;
}
if ( right<heapsize && a[max]<a[right] ){
max=right;
}
if (i!=max)
{
int temp=0;
temp=a[i];
a[i]=a[max];
a[max]=temp;
Buildheap(a,max,heapsize)
}
}
然后,建立大顶堆从最右边第一个非叶子节点开始,然后不断上移(编号 -1)。
for (int i=len/2-1;i>=0;i--)
{
Buildheap(a,i,len);
}
len是待排序列长度。所以上面这个for循环就完成了建大顶堆的过程。
下面就是利用大顶堆来排序(其实就是筛选出最大值)
for (int i=len-1;i>0;i--)
{
int temp=0;
temp=a[0];
a[0]=a[i];
a[i]=temp;
Buildheap(a,0,i);
}//每次将堆顶的值(最大值)放到数组的最后一位,然后将数组长度减一,再次进行建立大顶堆的操作,使得堆顶依然是相对于前一次最大值的次最大值,再次将这个次最大值放置到数组最后一位(此时的最后位置),依此规律循环,最后使得数组从小到大排序完成。
每次把堆顶元素放到数组最后,把数组最后的元素放到堆顶。记得每次数组长度缩减1(i--)。
所以核心就是两个for循环。一个用于建大顶堆,一个把大顶堆用来筛选就次大值。用的都是同一个程序Buildheap()。
完整代码:
public class HeapSort {
public static void buildHeap(int[] arr,int i,int heapSize){
//父子节点的编号规律
int left = 2*i +1;
int right = 2*i +2;
int max = i;
//比较父节点和左右子节点的大小
if(left < heapSize && arr[max]<arr[left]){
max = left;
}
if( right<heapSize && arr[max]<arr[right]){
max = right;
}
//若不满足大顶堆,交换父子节点,为了避免交换后子节点往下也不满足大顶堆,因此再进行一次建堆操作
if(i!=max){
int temp = arr[i];
arr[i] = arr[max];
arr[max] = temp;
buildHeap(arr,max,heapSize);
}
}
public static void heapSort(int[] arr,int len){
//由最后一个非叶子节点的节点开始向上遍历,依次建立大顶堆
for (int i = len/2-1;i>=0;i--){
buildHeap(arr,i,len);
}
//筛选最大值,排序
for(int j=len-1;j>0;j--){
int temp = arr[0];
arr[0] = arr[j];
arr[j] = temp;
buildHeap(arr,0,j);
}
}
public static void main(String[] args) {
int arr[] = {10,9,8,7,6,5,9,0,7,6,9,66,4,76,764,8,99,45,7,3,8,1};
for(int i:arr){
System.out.print(i+" ");
}
System.out.println();
heapSort(arr,arr.length);
for(int i:arr){
System.out.print(i+" ");
}
}
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。
其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。 ,空间复杂度是O(1)。堆排序主要在于理解堆的构造过程和在输出最大元素后如何对堆进行重新调整。也就是这两个核心的for循环过程。
堆排序,它的运行时间主要是消耗在构建堆和在重建堆时的反复筛选上。在构建堆的过程,因为我们是从完全二叉树最下层的非叶子结点开始构建的,将它与其孩子结点进行比较和有必要的互换,对于每个非叶子结点来说,其实最多2次比较和互换,故初始化堆的时间复杂度为O(n)。在正式排序的时候,第i次取堆顶记录和重建堆需要O(logi)的时间(完全二叉树的某个结点到根结点的距离为log2i+1),并且需要取n-1次堆顶记录,因此重建堆的时间复杂度为O(nlogn)。所以总的来说,堆排序的时间复杂度为O(nlogn)。由于堆排序对元素记录的排序状态不敏感,因此它无论最好,最坏,和平均时间复杂度均为O(nlogn)。
5.插入排序
直接插入排序
基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
核心在这个while循环,当后面的元素要移动的时候,可以会要一直向前移动,这个while就实现了这步操作。
直接插入排序是稳定排序;算法复杂度为O(n^2)
实现代码:
void sort(vector<int > &a){
for(int i=1;i<a.size();i++){
if(a[i]<a[i-1])//后面比前面小才动
{
int temp=a[i];
int j=i;
while(j>=1 && a[j-1]>temp)
{
a[j]=a[j-1];
j--;
}
a[j]=temp;
}
}
}
java实现
public static void insertionSort(int[] arr) { for (int i = 1; i < arr.length; i++) { int j = i; while (j > 0 && arr[j] < arr[j - 1]) { swap(arr,j,j-1); //这里由于直接调用swap所以看起来比上面的C代码简洁 j--; } } }