例如:待排序序列 5,4,3,7,2,7,(目的是升序排列)
1.冒泡排序:
排n趟,每趟只是将原序列变得相对有序,随着趟数的增加,有序性也得到提升,最终完全有序
5,4,3,7,2,7
第一趟,5>4,交换---》 4,5,3,7,2,7
5>3 ,交换---》 4,3,5,7,2,7
5<=7,不交换
7>2 ,交换,---》4,3,5,2,7,7
第一趟冒泡结果:4,3,5,2,7,7 , (其中 4,5,7,7有序)
第二趟冒泡结果(重复第一次的比较方式):3,4,2,5,7,7(其中3,4,5,7,7有序)
第三趟冒泡结果:3,2,4,5,7,7(其中3,4,5,7,7和2,4, 有序)
第四趟冒泡结果:2,3,4,5,7,7(其中2,3,4,5,7,7有序)
第五趟冒泡:没有发生交换,冒泡到此结束
规律:每趟比较都是将大的元素交换到后面
第一趟比较,就能将最大元素交换到最后
第二趟比较,就能将第二大元素交换到倒数第二个位置
******
那么每趟比较的元素数量是呈现 --1的趋势,这和插入排序有点像
java代码实现:
public int [] testBubbleSort(int []data){
int maxPos=0;
for(int i=0;i<data.length;i++)
{
// data.length-1-i及其之后的元素是排好序的
for(int j=0;j<data.length-i-1;j++)
{
if(data[j]>data[j+1])
{
//将最大位置与i交换
int temp =data[j];
data[j]=data[j+1];
data[j+1]=temp;
}
}
}
return data;
}
优化:
- 某趟排序未发生交换,表示排序已经完成
- 记录上一次的最后一次交换记录,减小下一次的扫描区间:
/ 比如 初始是 1,-1,3,5,2,7,8,9 //第一趟扫描之后:-1,1,3,2,5 ,7,8,9 最后一次2交换位置是(下标) 3 //第二趟扫描之后: -1,1,2,3,5,7,8,9 最后一次2交换位置是2(下标) 扫描范围也应该是 [0,3] 而不是[0,到length]
优化版本java代码:
/**
* @author wangwei
* @date 2019/1/24 18:49
* @classDescription 冒泡排序:
* 思想:每趟比较,将较大的向后交换,有序子序列从后往前扩张
* 稳定性:稳定
*/
public class BubbleSort implements Sortable {
@Override
public void sort(int[] array) {
//这只是一个小小的优化
//优化1:冒泡排序结束的条件:最后一趟没有发生交换
//优化2:第i趟发生的最后一次交换位置未被记录,导致第i+1 趟会重复 扫描未上一次没有发生交换的位置
// 比如 初始是 1,-1,3,5,2,7,8,9
//第一趟扫描之后:-1,1,3,2,5 ,7,8,9 最后一次2交换位置是(下标) 3
//第二趟扫描之后: -1,1,2,3,5,7,8,9 最后一次2交换位置是2(下标) 扫描范围也应该是 [0,3] 而不是[0,到length]
boolean exchanged = true;
int m = array.length-1;
while (m > 1 && exchanged) {
exchanged = false;
int lastSwapIndex = 1;//记录最后一次交换位置,初始化为1
for (int j = 0; j <= m-1; j++) {
if (array[j] > array[j + 1]) {
ArrayUtil.swap(array, j, j + 1);
exchanged = true;
lastSwapIndex = j;
}
}
m = lastSwapIndex;
if (!exchanged) {
return;
}
}
}
}
2.选择排序:
使用一个单位的空间来存放当前比较对象序列中的第一个元素,该趟的目标是找出这段比较序列中的最小元素及其下标,将其交换到这段比较序列头部
第一趟:比较序列是 5,4,3,7,2,7
初始minPos=0;
minVal=array[0]=5
在这一趟比较中,会出现 4<minVal=5 需要更新最小值:minVal=4,minPos=1
3<minVal=4 需要更新最小值:minVal=3,minPos=2
2<minVal=3 需要更新最小值:minVal=2,minPos=4
第一趟比较完之后,将第一趟找出的最小元素交换到序列头部
也就是 第一趟比较之前: 5,4,3,7,2,7,
第一趟比较之后交换结果:2,4,3,7,5,7
第二趟比较序列是:2, 4,3,7,5,7
比较交换后结果: 2,3,4,7,5,7
第三趟比较序列是:2,3,4,7,5,7
第i趟比较序列是:
array[i-1],array[i],....,array[array.length-1]
每比较完一趟,下一趟比较的元素数量比上一趟少一个
java代码实现
**
* @author wangwei
* @date 2019/1/24 10:47
* @classDescription 简单选择排序
* 排序思想:从无序子序列中"选择"关键字最小(升序排列)或最大的记录,并将其加入到有序子序列中,以此扩充有序子序列长度
* 排序稳定性:不稳定,由于使用了交换
*/
public class SimpleSelectSort implements Sortable{
@Override
public void sort(int[] array) {
for(int lastSortedIndex=0;lastSortedIndex<array.length;lastSortedIndex++){
int minIndex=lastSortedIndex;
for(int lookIndex=lastSortedIndex;lookIndex<array.length;lookIndex++){
if(array[lookIndex]<array[minIndex]){
minIndex=lookIndex;
}
}
if(minIndex!=lastSortedIndex){
ArrayUtil.swap(array,lastSortedIndex,minIndex);
}
}
}
}
3,插入排序:
模拟摸牌的过程,手中的牌始终是有序的,需要申请空间array.length(这个也是不一定的,如果直接将原数组在逻辑上分为有序与无序的两部分,也就不需要分配array.length 的空间来单独存储有序序列)
空间复杂度:o(1)
时间复杂度:o(n^2)
稳定性:稳定
java代码实现:
/**
* @author wangwei
* @date 2019/1/20 13:26
* @classDescription 实现简单插入排序
* 排序思想:模拟摸牌,手中牌始终是有序的,新元素插入手中,需要判断是否需要挪动
* 空间:o(1)
* 时间:o(n^2)
* 稳定性:稳定
*/
public class SimpleInsertSort implements Sortable{
@Override
public void sort(int[] array) {
int currIndex=1;///标记有序和无序元素的分界点 [0,currIndex) 表示有序区间
//随着有序区间不断的扩增,直到完全覆盖整个数组,表示完成排序
//currIndex处元素即为待排序元素
for(;currIndex<array.length;currIndex++){
int toBePut=array[currIndex];
//如果是值在有序数组范围内,需要考虑挪动,放入
if(toBePut<array[currIndex-1]){
//leftIndex表示有序序列最大下标
int leftIndex = currIndex-1;
while ((leftIndex >= 0) && (array[leftIndex] > toBePut)) {
array[leftIndex + 1] = array[leftIndex];
leftIndex--;
}
array[leftIndex+1]=toBePut;
}
}
}
/**
* 思考简单插入排序的改进:
* 1,查找新元素的适合位置 时,需要o(n)次比较,可以使用二分查找,改进为lg(n)
* 2,减少排序的规模:使用希尔排序,划分组
*/
}
生成随机数组的代码:
public static int []getArrayRand(int length)
{
int []result=new int[length];
for(int i=0;i<result.length;i++)
{
result[i]=(int)(Math.random()*10000);
}
return result;
}
此时速率比较:快排>插入排序>选择排序>冒泡排序
20万条元素排序时间(毫秒):
插入: 5593
选择:1,6547(冒泡的改良,避免了大量无效的交换)
冒泡:8,2772(慢如蜗牛,主要原因是大量无效的交换)
快速:47(飞一样的感觉)
4.归并排序
思想:将大规模的数组不断地二分,直到每个分区是一个元素,然后执行两个有序分区间的合并,直到合并成一个分区,完成排序
时间复杂度:o(nlgn) ,每趟归并时间复杂度是o(n) ,总共需要归并lgn 趟
空间复杂度:o(n)
java代码实现:
注意这里使用了两个版本
第一个是直接每次merge时,申请空间,存储归并后的有序结果,然后拷贝至原数组对应区间
第二个版本是排序开始之前,就申请了一个与待排序数组等大的数组tempArrays,每次归并操作都是将有序结果合并在tempArrays然后将数据拷贝至原数组对应区间
前者需要的空间是:o(nlgn)
后者只需要:o(n)
/**
* @author wangwei
* @date 2019/1/24 15:09
* @classDescription 归并排序
* 思想:将整体无序的原数组,分成若干规模小到可以直接排序的分组,每个分组完成排序后,两两合并(两个有序数组的合并)
* 最终达到整体有序的状态
*/
public class MergeSort implements Sortable {
@Override
public void sort(int[] array) {
int []tempArray=new int[array.length];
// mergeSort(array,0,array.length-1);
mergeSort2(array,tempArray,0,array.length-1);
}
void mergeSort(int[] array, int leftPos, int rightPos) {
if(leftPos<rightPos){
int center=(leftPos+rightPos)/2;
mergeSort(array,leftPos,center);
mergeSort(array,center+1,rightPos);
merge(array,leftPos,center+1,rightPos);
}
}
//执行合并操作
/**
*
* @param array
* @param leftPos 第一个有序区域的起始索引
* @param rightPos 第二个有序区域的起始索引
* @param rightEnd 第二个有序区域的结束索引
*/
void merge(int []array,int leftPos,int rightPos,int rightEnd){
final int leftEnd= rightPos-1;//第一个有序区域的最后的一个下标
final int eleCount=rightEnd-leftPos+1;
final int start=leftPos;
// int tempPos=leftPos;
int []tempArrays=new int[eleCount];
int tempPos=0;//tempArrays索引
while (leftPos<=leftEnd && rightPos<=rightEnd){
if(array[leftPos]<=array[rightPos]){
tempArrays[tempPos++]=array[leftPos++];
}else{
tempArrays[tempPos++]=array[rightPos++];
}
}
//处理两个数组合并之后仍然有一个数组剩余数据,直接放入tempArray
while (leftPos<=leftEnd){
tempArrays[tempPos++]=array[leftPos++];
}
while (rightPos<=rightEnd){
tempArrays[tempPos++]=array[rightPos++];
}
//拷贝tempArray 到原数组中
//注意只是:区间 leftPos至rightEnd区间 原数组的此区间元素已经归并放在tempArrays中
// System.arraycopy(tempArray,0,array,0,array.length);
RandomUtil.printArray(tempArrays);
for(int i=0;i<eleCount;i++){
array[start+i]=tempArrays[i];
// System.out.println(tempArray[rightEnd]);
}
}
void mergeSort2(int []array,int []tempArray,int leftPos,int rightPos){
if(leftPos<rightPos){
int center=(leftPos+rightPos)/2;
mergeSort2(array,tempArray,leftPos,center);
mergeSort2(array,tempArray,center+1,rightPos);
merge2(array,tempArray,leftPos,center+1,rightPos);
}
}
void merge2(int []array,int []tempArrays,int leftPos,int rightPos,int rightEnd){
final int leftEnd= rightPos-1;//第一个有序区域的最后的一个下标
final int eleCount=rightEnd-leftPos+1;
int tempPos=leftPos;
while (leftPos<=leftEnd && rightPos<=rightEnd){
if(array[leftPos]<=array[rightPos]){
tempArrays[tempPos++]=array[leftPos++];
}else{
tempArrays[tempPos++]=array[rightPos++];
}
}
//处理两个数组合并之后仍然有一个数组剩余数据,直接放入tempArray
while (leftPos<=leftEnd){
tempArrays[tempPos++]=array[leftPos++];
}
while (rightPos<=rightEnd){
tempArrays[tempPos++]=array[rightPos++];
}
//拷贝tempArray 到原数组中
//注意只是:区间 leftPos至rightEnd区间 原数组的此区间元素已经归并放在tempArrays中
RandomUtil.printArray(tempArrays);
for(int i=0;i<eleCount;i++,rightEnd--){
array[rightEnd]=tempArrays[rightEnd];
}
}
}
5.快速排序:
两个标记i,和j分别指向数组两端
选取一个k值,表示比较的对象,枢纽
目的是为了将比k小的元素,放到k的左侧,比k大的元素放在k的右侧
待排序数组:
i | j | ||||||||
下标 | 0 | 1 | 2 | 3 | 4 | 5 | |||
元素 | 5 | 4 | 3 | 7 | 2 | 7 |
i=0,j=5
k=array[i]=5;
part1 从右往左比较:
j=5时,array[ j ]=7>k,不交换
j--;
j=4时,array[ j ]=2<k=5,交换
目前数组:
i | j | ||||||||
下标 | 0 | 1 | 2 | 3 | 4 | 5 | |||
元素 | 2 | 4 | 3 | 7 | 5 | 7 |
k=array[ j ] =5;
part2 从左往右比较
此时:i=0;j=4
k=5>array[i]=2,不交换
i++
k=5>array[i]=array[1]=4,不交换
i++
k=5>array[i]=array[2]=3,不交换
i++
k=5<=array[i]=array[3]=7,交换
交换结果:
i | j | ||||||||
下标 | 0 | 1 | 2 | 3 | 4 | 5 | |||
元素 | 2 | 4 | 3 | 5 | 7 | 7 |
现在从右往左比较,回到part1
k=array[i]=array[3]=5
k=5<array[j]=7,不交换
j--
出现i和j碰头,结束以k=5的比较,对k=5这一元素的左右两侧分别执行上述操作,part1+part2
第一次快排的结果:
i | j | ||||||||
下标 | 0 | 1 | 2 | 3 | 4 | 5 | |||
元素 | 2 | 4 | 3 | 5 | 7 | 7 |
数组1:2,4,3
数组2:7,7
5,4,3,7,2,7,
java代码实现
public void quickSort(int []array,int from,int to){
if(from<0||to>=array.length){
return;
}
if(from>=to){
return;
}
int kVal=array[from];
int kPos=from;
// int kPos=(from+to)/2;
// int kVal=array[kPos];
int low=from;
int high=to;
while(low<high){
//从右往左找比kVal小的元素
while(low<high && array[high]>=kVal ){
high--;
}
//从右往左出现比kVal小的元素了,交换
if(high>low ){
array[kPos]=array[high];
// array[high]=kVal;
kPos=high;
low++;
}
//从左往右,找比kVal大的元素
while(low<high && array[low]<=kVal ){
low++;
}
//找到比kVal大的元素,交换
if(low<high ){
array[kPos]=array[low];
// array[low]=kVal;
kPos=low;
high--;
}
}
array[kPos]=kVal;
//到此时,kPos左边全是比kVal小的元素
// 右边全是比kVal大的元素
quickSort(array,from,kPos-1);
quickSort(array,kPos+1,to);
}
快排的优化在于:枢纽的选取,尽可能居中,这样就能减少递归的次数
选取枢纽的方式:
- 取首元素或者是尾元素(如果完全逆序或者是顺序,则会性能急剧下降,退化成冒泡排序)
- 随机数法(较耗时)
- 三者取中法(比较平衡,相比1,2)
三者取中 java代码实现:
/**
* @author wangwei
* @date 2019/1/24 18:58
* @classDescription 快速排序 思想: 不断地选取枢纽("中间值"),大,小的元素分居两侧,
* 比如: 1,4,3,5,6,2
* 假设选取 3 为中间值
* 第一趟之后:1,2,3,5,6,4
* 稳定性:不稳定
* <p>
* <p>
* 需要注意是:优化需要考虑 "中间值"的选取方式
* 常用的选取方式:1,取首元素(不一定能取到靠中的位置,比如 1,2,3,4,5,6) 有序与逆序情况这是糟糕的
* 2,随机数法 (比较耗时)
* 3,三者取中(耗时短)
* <p>
* 待排序数组的特点也会影响排序效率:如果数组接近有序,快排会退化为冒泡排序
*/
public class QuickSort implements Sortable {
@Override
public void sort(int[] array) {
qkSort(array, 0, array.length - 1);
// quickSort(array,0,array.length-1);
}
void qkSort(int[] array, int low, int high) {
if (low >= high) {
return;
}
//获取枢纽值,并将其放在当前待处理序列末尾
//这里的keyPos 是选取的三者取中的方法,当然也可以采用随机数,(耗时)),取首元素(对于完全有序或逆序,会很差的性能)
dealPivot(array, low, high);
int keyPos = high - 1;
System.out.println(keyPos);
int keyVal = array[keyPos];
int left = low +1;
int right = high-1;
while (left < right) {
//从左往右找到第一个比kVal大的元素 x1
while ((left < right) && array[left] <= keyVal) {
left++;
}
//从右往左找到第一个比kVal 小的元素 x2
//从右往左,大的往右交换
while ((left < right) && array[right] >= keyVal) {
right--;
}
//交换 x1,x2
if (left < right) {
ArrayUtil.swap(array, left, right);
}
}
//防止出现left与right碰头处元素大于kVal,如果不发生交换,此次排序相当于无用
if (array[right] > keyVal) {
ArrayUtil.swap(array, right, keyPos);
}
qkSort(array, low, right - 1);
qkSort(array, right + 1, high);
}
// 三者取中 将 两端 与中间位置元素比较,将中间大小的数据放在数组倒数第二个位置,最小放在开头,最大放在末尾
private void dealPivot(int[] array, int left, int right) {
//比如:1 4 2 4 7 3 8 首:1,中:4,尾:8 排序为 1,4,8 调整后数据变为 1,4,2,7,3,4,8
int mid = (left + right) / 2;
if (array[mid] > array[right]) {
ArrayUtil.swap(array, mid, right);
}
if (array[left] > array[right]) {
ArrayUtil.swap(array, left, right);
}
if (array[left] > array[mid]) {
ArrayUtil.swap(array, left, mid);
}
ArrayUtil.swap(array, mid, right - 1);
}
总结:
1,冒泡排序:每趟总是从左至右与相邻元素比较,一趟会将当前待排序序列中的最大的那个元素交换到最后
(注意:并不一定每次只是完成一个元素的排序,也就是该趟的最后一次交换位置,可以记住,减少下次排序规模)
2,选择排序:与冒泡类似,但是不会频繁的交换元素,使用一个空间记录当前待排序序列中的最大值,该趟扫描完,放到序列最后,待排序规模-1
3,插入排序:类比摸取扑克牌,申请一个输入规模一样大的空间(也可以不申请,只是在原数组逻辑上分为有序与无序区间),存储已排好序的元素
4,归并排序:将大数组不断二分,将各个有序子数组归并成一个有序数组,最终完成排序
5,快速排序:通过分而治之的思想排序。