综述
- 插入排序:直接插入,折半插入,希尔排序;
- 交换排序:冒泡排序,快速排序;
- 选择排序:简单选择,堆排序;
- 2路归并
- 基数排序
- 外部排序
- 算法分析
1.插入排序
1.1直接插入:
思想
- 插入a,向前寻找比自己大的结点;
- 将a保存在哨兵位置;
- 边找边向后移动;
- 将哨兵元素赋值给找到的结点
代码
void InsertSort(ElemType A[],int n)
{
int i,j;
for(i=2;i<=n;i++)
{
A[0]=A[i];
for(j=i-1;A[j]>A[0];j--) { A[j+1]=A[j]; }
A[j+1]=A[0];
注意最后一次循环,A[j+1]=A[j]执行结束后,再次执行j--;
所以需要A[J+1];
}
}
复杂度:
S:O(1);
T:O(n^2);
1.2 折半插入:
思想:查找时用折半
代码:
void InsertSort(ElemType A[],int n)
{
int i,j,left,right,mid;
for(i=2;i<=n;i++)
{
A[0]=A[i];
left=1,right=i-1;
while(left<=right)
// 这里写成left<right则不稳定。等号是为了left和right相等时,继续向右搜索是否有相等元素,保持稳定性。
{
mid= (left+right )/2;
if(A[mid]>A[0])right=mid-1;
else left=mid+1;
//A[mid]==A[0]为了保证稳定性,继续右移;
}
出循环:则必有left>right。
for(j=i-1;j>=left;--j) { A[j+1]=A[j] ;}
A[left]=A[0];
}
}
复杂度:
S:O(1);
T:O(n^2), 查找为(nlog(2,n));移动为(O(n^2))取最大
1.3希尔排序
思想:
按dk分组,组内采用直接插入排序
image.png
代码:
void ShellSort(ElemType A[],int n)
{
int i,j,dk;
for(dk=n/w;dk>=1;dk=dk/2)
{
for(i=dk+1;i<=n;i++)
{
if(A[i]<A[i-dk])
{
A[0]=A[i];
for(j=i-dk;j>0&&A[i]<[j];j=j-dk) { A[j+dk]=[j] ;}
A[j+dk]=A[0];
}
}
}
}
分析
- 复杂度: S:1,T:n^2;
- 稳定性:不稳定;
- 适用性:直接插入(链表和顺序表都可以),希尔和折半(只可顺序)
2.交换排序
2.1冒泡排序
void BubbleSort(ElemType A[],int n)
{
int flag;
ElemType temp;
for(int i=0;i<n;i++)
{
flag=0;
for(int j=n-1;j>i;j--)
{
if(A[j]<A[j-1]])
{
temp=A[j];
A[j]=A[j-1];
A[j-1]=temp;
flag=1;
}
}
if(!flag) return;
}
}
2.2快速排序
思想:
- 根据二叉插入法,不难看出,即便搜索合适位置需要时间为Nlog(N),移动元素也需要N^2,因此想要优化时间复杂度,必须从移动元素和查询合适位置两方面双管齐下。
- 快排基于分治法实现。
步骤:
- partion函数:找到pivot,将当前序列分成两部分。
- 递归分治。
- partition函数思想:一遍二分查位置一遍移动。两个指针,若在后方出现小元素,复制到前指针;若前方出现大元素,复制到后指针。
代码:
void QuickSort(ElemType A[],int int left,int right )
{
int pivot=Partition(A,left,right);
QuickSort(A,left,pivot-1);
QuickSort(A,pivot+1,right);
}
ElemType Partition(ElemType A[],int left,int right)
{
int pivot=A[left];
while(left<right)
{
while(left<right&&A[right]>pivot) right--;
A[left]=A[right];
while(left<right&&A[left]<pivot) left++;
A[right]=A[left];
}
A[left]=pivot;
return left;
}
性能分析:
- 时间复杂度:平均(nlog(n)),最坏(n^2);
若序列基本有序,快排将会较慢。 - 稳定性:不稳定。
王道习题
1.双向冒泡
- 循环判定条件写的比较巧妙: !flag&&left<right
void BubbleSort(ElemType A[],int n)
{
int left=0,right=n-1;
int flag;
while(!flag&&left<right)
{
flag=0;
for(int i=right;i>left;i--)
{
if(A[i]<A[i-1]) { swap(A[i],A[j]); flag=1; }
}
left++;
for(int j=left;j<right;j++)
{
if(A[j]>A[j+1]) { swap(A[j],A[j+1]; flag=1;) }
}
right--;
}
}
- 把所有奇数移动到偶数前面
- 将序列分成偶数奇数并隔开,很容易想到快排;
- 代码:
ElemType Partition(ElemType A[],int left,int right)
{
int pivot=A[left];
while(left<right)
{
while(left<right&&!(A[right]%2)) right--;
A[left]=A[right];
while(left<right&&A[left]%2) left++;
A[right]=A[left];
}
A[left]=pivot;
return left;
}
- 随机快排
思路
- 取随机index j,swap(A[j],A[left]),再执行正常快排;
void RQSort(ElemType A[],int left,int right)
{
int pivot=Partition(A,left,right);
RQSort(A,left,pivot-1);
RQSort(pivot+1,right);
}
int Partition(ElemType A[],int left,int right)
{
int Rindex=rand(left-right)+left;
swap(A[Rindex],A[left]);
ELemType pivot=A[left];
while(left<right)
{
while(left<right&&A[right]>pivot]) rigth--;
A[left]=A[right];
while(left<right&&A[left]<pivot) left++;
A[right]=A[left];
}
A[left]=pivot;
return left;
}
- 找到第k小的元素
- 快排修改版,若左边的元素个数为k-1即pivot为所寻找的值
- 代码
ElemType GetKth(ElemType A[],int left,int right)
{
Elem pivot=A[left];
int low_temp=low,right_temp=right;
while(left<right)
{
while(left<right&&A[right]>pivot]) rigth--;
A[left]=A[right];
while(left<right&&A[left]<pivot) left++;
A[right]=A[left];
}
if(==k-1)return pivot;
return pivot>k-1 ? GetKth(A,left_temp,left-1) : GetKth(A,left+1,right_temp);
}
- 将一个原序列划分为两个,数量差最小,类和差最大的子序列:
思想:
- 快排中的partition的改变,让pivot接近n/2.ground;
代码:
void setPartition(int A[],int n)
{
int pivot=A[0],left=0,right=n-1;
int p=left;
while(p!=n/2)
{
left=0,right=n-1;
while(left<right&&A[right]>pivot) right--;
A[left]=A[right];
while(left<right&&A[left<pivot]) right++;
A[right]=A[left];
p=left;
}
}
空间复杂度:O(1)
时间复杂度:O(n)
7. 荷兰国旗
题干:将乱序多个的红色,白色,蓝色条块排序成:红-白-蓝的荷兰国旗。(即红色的排在一起,白色的排在一起)
要求:时间复杂度为O(N)
思想:partition变体。
代码:
typedef enum color {red,blue,white}color;
void Flag_arrange(color a[],int n)
{
int i=0,j=0,k=n-1;
while(j<=k)
{
switch(a[j])
{
case(red): swap(a[i],a[j];i++;j++);break;
case(white):j++;break;
case(blue) swap(a[j],a[k]);k--;break;
}
}
}
3.选择排序:简单选择排序+堆排序
3.1简单选择排序:
- 思想 :从子序列中找最小值(n),放入子序列最前端。子序列长度为1时停止。
- 代码:
void SelectSort(ElemType A[],int n)
{
int min=0,i=0,j=0;
for(;i<n-1;i++)
for(int j=i;j<n-1;j++)
{
if(A[j]<A[min])min=j;
}
if(j!=i) {swap(A[i],A[min]);}
}
3.2堆排序:
- 思想:
以大根堆为例,要建立一个根节点大于所有结点的树,T为O(n);
对堆排序的T为nlogn - 步骤:
对非叶节点(i<=len/2.ground)进行“下坠”操作,即HeadAdjust。
每次HeadAdjust比较次树不会超过2(h-i),即小于2log(2,n);
将顶点元素和末尾元素换位,重新排序(树结点数量减一)。
A[0]暂存最大值
重复上述步骤,直到得到一棵先序遍历结果为递增序列的树。
- 建堆
Void BuildHeap(Elemtype A[],int len)
{
for(int i=len/2;i>0;i--){HeadAdjust(A,i,len);}
}
void HeapSort(int A[],int len)
{
BuildHeap(A,len);
for(int i=len;i>1;i--)
{
swap(A[i],A[1]);
HeadAdjust(A,1,i-1);
}
}
下坠操作:
void HeadAdjust(int A[],int k,int len)
{
A[0]=A[k];
for(int i=2*k;i<=len;i=i*2)
{
if(i<len&&A[i]<A[i]+1)i++; //找到最大孩子;
if(A[0]>A[k])break; //若比最大孩子大,则k可以作为A[0]的存入点
// else
A[k]=A[i]; 否则,继续向下寻找存入点。// 同时将最大孩子上浮。
k=i;
}
A[k]=A[0];
}
下坠操作的简图:
image.png
时间复杂度:4n(建堆)+nlog(2,n)(排序)
王道习题:
-
算法分析,取出第k个最小元素之前的部分有序序列,采用什么方法:
简单选择排序:每次选择最小,时间复杂度为kn。(每趟选出一个最小,每趟为n)
冒泡排序:每次选择最小,时间复杂度为kn
堆排序:建堆的时间为4n,取k个最小的时间为klog2n,总时间为4n+log(2,n)。
k和n很小时用简单排序/冒泡排序,k和n很大时,用堆排序。 -
单链表实现简单选择排序:
思路:头插。
代码:
void SelectSort(LinkList L)
{
Lnode* pre,*p,*tail,*s;
pre=L;
int flag;
while(pre!=null)
{
p=pre - > next,tail=p - > next;
flag=0;
while(tail!=null)
{
if(tail - > data<p - > data) {p=tail;s=p;tail=tail - > next; flag=1; }
else {tail=tail - > next;}
}
if(flag) break;
s - > next=p - > next;
p - > next=pre - > next;
pre=p;
}
}
- 判断小根堆
思路:
- A[0]为temp。关键字的范围是[1,n]
- 对所有分支结点,判断其与子节点的关键字。
代码:
bool IsMinHeap(ElemType A[],int len)
{
if(len%0==0)//偶数长度,则节点有奇数个。有一个但分支结点。
{
if(A[len]<A[len/2])return false;
for(int i=len/2-1;i>=1;i--)
{
if(A[i]<A[i/2])return false;
}
}
else
{
for(int i=len/2-1;i>=1;i--)
{
if(A[i]<A[i/2])return false;
}
}
return true;
}
4.归并排序和基数排序
4.1 归并排序
- 思想
分治 - 代码:
void MergeSort(ElemType A[],int left,int right)
{
if(left<right)
{
int mid=(left+right)/2;
MergeSort(A,left,mid-1);
MerseSort(A,mid,right);
Merge(A,low,right,mid);
}
}
ElemType *B=(ElemType*)malloc(sizeof((n+1)*ElemType));
void Merge(ElemType A[],int left,right,mid)
{
for(int i=left;i<right;i++) { B[i]=A[i]; }
int i,j,k;
for( i=low,j=mid,k=i;j<=right;k++)
{
if(B[i]<B[j]){A[k]=B[i++];}
if(B[i>=B[j]]){A[k=B[j++];}
}
while(i<=mid) { A[k++]=B[i++]; }
while(j<=right) { A[K++]=b[j++]; } //元素相等,覆盖后也相等,这里保证了排序的稳定性
}
C++:
vector<int> B(A.size(),0);
void MergeSort(vector<int>& A,int left,int right){
int mid=(left+right)/2;
MergeSort(A,left,mid);
MergeSort(A,mid+1,right);
Merge(A,left,right,mid);
}
void Merge(vector<int>& A,int left,int right,int mid){
for(int i=left;i<right;i++) { B[i]=A[i]; }
int i,j,k;
for(i=left,j=mid,k=i;i<=right;k++){
if(B[i]>B[j]){A[k]=B[i++];}
else {A[k]=B[j++];}
}
while(i<=mid){A[k++]=B[left++];}
while(j<=right){A[k++]=B[j++];}
}
4.2 基数排序
思想:
- 不基于比较
- 关键字位数相同(若不同,高位补零)
- 高位优先:MSD
- 低位优先:LSD
效率
空间O(R)
时间O(d(n+r))
5.算法分析
-
时空复杂度和稳定性:
image.png
-
- 2.阶段结果:(第n躺的结果)
冒泡排序,堆排序,选择排序每趟可以产生最大值/最小值;
快排每趟可以确定一个元素的最终位置。(即一次partition可以确定pivot的最终位置)