汇总了很多博客的内容,作为一个学习笔记~
概述排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
内部排序(使用内存):
插入排序(直接插入、希尔),选择排序(简单选择、堆),交换排序(冒泡、快速),归并排序,基数排序
(直接)插入排序(稳定)
思想:先给定一个排好序的序列(通常设定为给定要排序序列的第一个值),然后陆续将后面的值与前面排好序的比较,如果是小于前面的值,就插到前面去,直到到达最终位置(即该变量不小于它的前驱)。每次遍历的任务是:通过扫描前面已排序的子列表,将位置i处的元素定位到从0到i的子列表之内的正确的位置上。
复杂度:空间复杂度O(1),时间复杂度O(n2) 。最坏情况和平均情况运行时间都为O(n^2)
最差情况:反序,需要移动n*(n-1)/2个元素 ,最好情况:正序,不需要移动元素。
性质:数组在已排序或者是“近似排序”时,插入排序效率的最好情况运行时间为O(n);
序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。
通常,插入排序呈现出二次排序算法中的最佳性能。
public static void InsertSort(int[] arr)
{
int i, j;
int n = arr.Length;
int target;
for (i = 1; i < n; i++){ //假定第一个元素被放到正确位置上,仅需遍历1-(n-1)
j = i;
target = arr[i];
while (j > 0 && target < arr[j-1]){
arr[j] = arr[j-1];
j--;
}
arr[j] = target;
}
}
希尔排序(不稳定):
基本思想:也称为“缩小增量排序”,先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
采用跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
基本有序:就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间,例如{2,1,3,6,4,7,5,8,9,}就可以称为基本有序了。但像{1,5,9,3,7,8,2,4,6}这样,9在第三位,2在倒数第三位就谈不上基本有序。
操作方法:选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;按增量序列个数k,对序列进行k 趟排序;每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
复杂度:最好时间复杂度和平均时间复杂度都是O(nlogn),最坏时间复杂度为O(n^1.5).
public static void sort(int[] arrays){
if(arrays == null || arrays.length <= 1){
return;
}
int incrementNum = arrays.length/2;//增量
while(incrementNum >=1){
for(int i=0;i<arrays.length;i++){//进行插入排序
for(int j=i;j<arrays.length-incrementNum;j=j+incrementNum){
if(arrays[j]>arrays[j+incrementNum]){
int temple = arrays[j];
arrays[j] = arrays[j+incrementNum];
arrays[j+incrementNum] = temple;
}
}
}
incrementNum = incrementNum/2;//设置新的增量
}
}
冒泡排序(Bubble Sort)(稳定)
原理:比较两个相邻的元素,将值大的元素交换至右端。
思路:依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。重复第一趟步骤,直至全部排序完成。(每完成一趟,就完成了把当前最大值放到了末尾)。
性质:N个数字要排序完成,总共进行N-1趟排序,每i趟的排序(比较)次数为(N-i)次,总共的比较次数(N-1)+(N-2)+...+1=N*(N-1)/2。
优点:每进行一趟排序,就会少比较一次,因为每进行一趟排序都会找出一个较大值。
时间复杂度:1.正序,只需要一趟即可完成排序。所需的比较次数C和记录移动次数M均达到最小值,即:Cmin=n-1;Mmin=0;所以,冒泡排序最好的时间复杂度为O(n)。
- 反序,则需要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:Cmax=n*(n-1)/2=O(n^2), Mmax=3*n*(n-1)/2=O(n^2)。所以,最坏的时间复杂度为O(n^2)。
- 综上所述冒泡排序总的平均时间复杂度为:O(n^2) 。
稳定性:相同元素的前后顺序并没有改变,稳定。
- public class BubbleSort {
public static void main(String[] args) {
int[] arr=args;
for(int i=0;i<arr.length-1;i++){//控制排序趟数
for(int j=0;j<arr.length-1-i;j++){//控制每一趟排序多少次
if(arr[j]>arr[j+1]){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
}
- 下面开始考虑优化,如果对于一个本身有序的序列,或则序列后面一大部分都是有序的序列,上面的算法就会浪费很多的时间开销,这里设置一个标志flag,如果这一趟发生了交换,则为true,否则为false。明显如果有一趟没有发生交换,说明排序已经完成。
public static void bubbleSort2(int [] a, int n){
int j, k = n;
boolean flag = true;//发生了交换就为true, 第一次判断时必须标志位true。
while (flag){
flag=false;//每次开始排序前,都设置flag为未排序过
for(j=1; j<k; j++){
if(a[j-1] > a[j]){//前面的数字大于后面的数字就交换
int temp;
temp = a[j-1];
a[j-1] = a[j];
a[j]=temp;
flag = true; //表示交换过数据;
}
}
k--;//减小一次排序的尾边界
}
}
- 再进一步做优化。比如,现在有一个包含1000个数的数组,仅前面100个无序,后面900个都已排好序且都大于前面100个数字,那么在第一趟遍历后,最后发生交换的位置必定小于100,且这个位置之后的数据必定已经有序了,也就是这个位置以后的数据不需要再排序了,于是记录下这位置,第二次只要从数组头部遍历到这个位置就可以了。如果是对于上面的冒泡排序算法2来说,虽然也只排序100次,但是前面的100次排序每次都要对后面的900个数据进行比较,而对于现在的排序算法3,只需要有一次比较后面的900个数据,之后就会设置尾边界,保证后面的900个数据不再被排序。
public static void bubbleSort3(int [] a, int n){
int j , k;
int flag = n ;//flag来记录最后交换的位置,也就是排序的尾边界
while (flag > 0){//排序未结束标志
k = flag; //k 来记录遍历的尾边界
flag = 0;
for(j=1; j<k; j++){
if(a[j-1] > a[j]){//前面的数字大于后面的数字就交换
int temp;
temp = a[j-1];
a[j-1] = a[j];
a[j]=temp;
flag = j;//记录最新的尾边界.
}
}
}
}
快速排序(不稳定)
概念:所谓排序就是比较两个关键字大小,然后将一个序列的记录从一个位置移动到另一个位置,以达到一个从小到大的有序的序列,快速排序属于交换排序的一种。
基本思想:在要排的数(比如数组A)中选择一个中心值key(比如A[0]),通过一趟排序将数组A分成两部分,其中以key为中心,key右边都比key大,key左边的都key小,然后对这两部分分别重复这个过程,直到整个有序。
优势:快速排序最“快”的地方在于左右两边能够快速同时递归排序下去,所以最优的情况是基准值刚好取在无序区的中间,这样能够最大效率地让两边排序,同时最大地减少递归划分的次数。此时的时间复杂度仅为O(NlogN)。但是,当得到的基准值总是当前无序区域里最大或最小的那个元素,这种情况下基准值的一边为空,另一边则依然存在着很多元素(仅仅比排序前少了一个),此时时间复杂度为O(N*N)。
特点:速度快慢关键在于基准值的选取,它决定了划分次数以及比较次数。
public static void quickSort(int[] a) {
if(a.length>0) {
quickSort(a, 0 , a.length-1);
}
}
private static void quickSort(int[] a, int low, int high) { //1,找到递归算法的出口
if( low > high) { return; }
int i = low; //2, 存
int j = high;
int key = a[ low ]; //3,key
while( i< j) { //4,完成一趟排序
while(i<j && a[j] > key){ //4.1 ,从右往左找到第一个小于key的数
j--;
}
while( i<j && a[i] <= key) { // 4.2 从左往右找到第一个大于key的数
i++;
}
if(i<j) { //4.3 交换
int p = a[i];
a[i] = a[j];
a[j] = p;
}
}
int p = a[i]; // 4.4,调整key的位置
a[i] = a[low];
a[low] = p;
quickSort(a, low, i-1 ); //5, 对key左边的数快排
quickSort(a, i+1, high); //6, 对key右边的数快排
}
}
(简单)选择排序(不稳定)
原理:每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。也就是:每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。基于此思想的算法主要有简单选择排序、树型选择排序和堆排序。
基本思想:给定数组int[] arr={里面n个数据};第1趟排序,在待排序数据arr[1]~arr[n]中选出最小的数据,将它与arrr[1]交换;第2趟,在待排序数据arr[2]~arr[n]中选出最小的数据,将它与r[2]交换;以此类推,第i趟在待排序数据arr[i]~arr[n]中选出最小的数据,将它与r[i]交换,直到全部排序完成。
复杂度:(1)假设待排序的序列有 N 个元素,则比较次数永远都是N (N - 1) / 2,与序列的初始排序无关。(2)移动次数与序列的初始排序有关。当序列正序时,移动次数最少,为 0。当序列反序时,移动次数最多,为3N (N - 1) / 2。综上简单排序的时间复杂度为 O(N^2)。
void SelectSort(int a[]) //选择排序
{
int mix,temp;
int n=a.length;
for(int i=0;i<n-1;i++) {//每次循环数组,找出最小的元素,放在前面 mix=i; //假设最小元素的下标
for(int j=i+1;j<n;j++){//将假设的最小元素与数组比较,交换出最小元素的下标
if(a[j]<a[mix]){
mix=j;
}
}
if(i!=mix) {//若数组中真的有比假设的元素还小,就交换
temp=a[i];
a[i]=a[mix];
a[mix]=temp;
}
}
}
因为简单选择排序没有利用上次选择时比较的结果,所以造成了比较次数多,速度慢。如果能够加以改进,将会提高排序的速度,所以出现了后面的树形选择排序和堆排序。
(树形)选择排序(Tree Select Sort)(不稳定)
思想:又叫锦标赛排序(Tournament Sort),是一种按照锦标赛思想进行选择排序的方法。首先对n个记录的关键字进行两两比较,然后在其中[n/2](向上取整)个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录为止。
过程:这个过程可以用一棵有n个叶子结点的完全二叉树表示。n个叶子结点中依次存放排序之前的n个关键字,每个非终端结点中的关键字均等于其左、右孩子结点中较小的那个关键字,则根结点中的关键字为叶子结点中的最小关键字。
由于含有n个叶子结点的完全二叉树的深度为[log2n]+1,则在树形选择排序中,除了最小关键字以外,每选择一个次小关键字仅需进行[log2n]次比较,因此,它的时间复杂度为O(nlogn)。但是,这种排序方法也有一些缺点,比如辅助存储空间较多,并且需要和“最大值”进行多余的比较。
public static void treeSelectSort(Object[] a){
int len = a.length;
int treeSize = 2 * len - 1; //完全二叉树的节点数
int low = 0;
Object[] tree = new Object[treeSize]; //临时的树存储空间
//由后向前填充此树,索引从0开始
for(int i = len-1,j=0 ;i >= 0; --i,j++){ //填充叶子节点
tree[treeSize-1-j] = a[i];
}
for(int i = treeSize-1;i>0;i-=2){ //填充非终端节点
tree[(i-1)/2] = ((Comparable)tree[i-1]).compareTo(tree[i]) < 0 ? tree[i-1]:tree[i];
}
//不断移走最小节点
int minIndex;
while(low < len){
Object min = tree[0]; //最小值
a[low++] = min;
minIndex = treeSize-1; //找到最小值的索引
while(((Comparable)tree[minIndex]).compareTo(min)!=0){
minIndex--;
}
tree[minIndex] = Integer.MAX_VALUE; //设置一个最大值标志
while(minIndex > 0){ //如果其还有父节点
if(minIndex % 2 == 0){ //如果是右节点
tree[(minIndex-1)/2] = ((Comparable)tree[minIndex-1]).compareTo(tree[minIndex]) < 0 ? tree[minIndex-1]:tree[minIndex];
minIndex = (minIndex-1)/2;
}else{ //如果是左节点
tree[minIndex/2] = ((Comparable)tree[minIndex]).compareTo(tree[minIndex+1]) < 0 ? tree[minIndex]:tree[minIndex+1];
minIndex = minIndex/2;
}
}
}
}
堆排序(不稳定)
思想:树形选择排序方法,将array看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(最小)的元素。
- 若array[0,...,n-1]表示一颗完全二叉树的顺序存储模式,则任意一节点指针 i:
父节点:i==0 ? null : (i-1)/2,并且左孩子:2*i + 1 右孩子:2*i + 2
2. 堆的定义:n个关键字序列array[0,...,n-1],当且仅当满足下列要求:(0 <= i <= (n-1)/2)
① array[i] <= array[2*i + 1] 且 array[i] <= array[2*i + 2]; 称为小根堆;
② array[i] >= array[2*i + 1] 且 array[i] >= array[2*i + 2]; 称为大根堆;
3. 建立大根堆:
n个节点的完全二叉树array[0,...,n-1],最后一个节点n-1是第(n-1-1)/2个节点的孩子。对第(n-1-1)/2个节点为根的子树调整,使该子树称为堆。对于大根堆,调整方法为:若【根节点的关键字】小于【左右子女中关键字较大者】,则交换。
之后向前依次对各节点((n-2)/2 - 1)~ 0为根的子树进行调整,看该节点值是否大于其左右子节点的值,若不是,将左右子节点中较大值与之交换,交换后可能会破坏下一级堆,于是继续采用上述方法构建下一级的堆,直到以该节点为根的子树构成堆为止。
反复利用上述调整堆的方法建堆,直到根节点。
4.堆排序:(大根堆)
①将存放在array[0,...,n-1]中的n个元素建成初始堆;
②将堆顶元素与堆底元素进行交换,则序列的最大值即已放到正确的位置;
③但此时堆被破坏,将堆顶元素向下调整使其继续保持大根堆的性质,再重复第②③步,直到堆中仅剩下一个元素为止。
复杂度:空间复杂度:o(1);时间复杂度:建堆:o(n),每次调整o(log n),故最好、最坏、平均情况下:o(n*logn);
1 //构建大根堆:将array看成完全二叉树的顺序存储结构
2 private int[] buildMaxHeap(int[] array){
3 //从最后一个节点的父节点(array.length-1-1)/2开始,直到根节点0,反复调整堆
4 for(int i=(array.length-2)/2;i>=0;i--){
5 adjustDownToUp(array, i,array.length);
6 }
7 return array;
8 }
9
10 //将元素array[k]自下往上逐步调整树形结构
11 private void adjustDownToUp(int[] array,int k,int length){
12 int temp = array[k];
13 for(int i=2*k+1; i<length-1; i=2*i+1){ //i为初始化为节点k的左孩子
14 if(i<length && array[i]<array[i+1]){ //取节点较大的子节点的下标
15 i++; //如果节点的右孩子>左孩子,则取右孩子节点的下标
16 }
17 if(temp>=array[i]){ //根节点 >=左右子女中关键字较大者,调整结束
18 break;
19 }else{ //根节点 <左右子女中关键字较大者
20 array[k] = array[i]; //将左右子结点中较大值array[i]调整到双亲节点上
21 k = i; //【关键】修改k值,以便继续向下调整
22 }
23 }
24 array[k] = temp; //被调整的结点的值放人最终位置
25 }
1 //堆排序
2 public int[] heapSort(int[] array){
3 array = buildMaxHeap(array); //初始建堆,array[0]为第一趟值最大的元素
4 for(int i=array.length-1;i>1;i--){
5 int temp = array[0];
//将堆顶元素和堆低元素交换,即得到当前最大元素正确的排序位置
6 array[0] = array[i];
7 array[i] = temp;
8 adjustDownToUp(array, 0,i); //整理,将剩余的元素整理成堆
9 }
10 return array;
11 }
基数排序(RadixSort)
归并排序(Merge Sort)
基本思想:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
平均时间复杂度:O(NlogN)归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。
public static void merge_sort(int a[],int first,int last,int temp[]){
if(first < last){
int middle = (first + last)/2;
merge_sort(a,first,middle,temp);//左半部分排好序
merge_sort(a,middle+1,last,temp);//右半部分排好序
mergeArray(a,first,middle,last,temp); //合并左右部分
}
}
//合并 :将两个序列a[first-middle],a[middle+1-end]合并
public static void mergeArray(int a[],int first,int middle,int end,int temp[]){
int i = first;
int m = middle;
int j = middle+1;
int n = end;
int k = 0;
while(i<=m && j<=n){
if(a[i] <= a[j]){
temp[k] = a[i];
k++;
i++;
}else{
temp[k] = a[j];
k++;
j++;
}
}
while(i<=m){
temp[k] = a[i];
k++;
i++;
}
while(j<=n){
temp[k] = a[j];
k++;
j++;
}
for(int ii=0;ii<k;ii++){
a[first + ii] = temp[ii];
}
}