算法练习-9种排序算法

排序算法的分类:

插入元素的排序:每次待排元素是以插入形式排好的,就和打扑克牌一样。

属于插入排序的有:直接插入排序,希尔排序(shell排序)。

选择排序:选择最大的或者最小的,然后放到尾部或者首部。

属于选择排序的有:选择排序,堆排序。

交换排序:排序时需要交换元素的排序。

属于交换排序的有:冒泡排序,快速排序

归并排序:将两个有序的序列合并成一个有序的序列

属于归并排序的有:归并排序


排序算法稳定性的判断:

如果Ai == Ai+1,前一个元素等于后一个元素,当排序完成之后,Ai和Ai+1的相对位置不变,则称这个排序为稳定的排序。否则为不稳定的排序。

稳定的排序:

冒泡排序,由于冒泡排序对于相邻的两个元素相等时不会交换顺序,所以,冒泡排序是一种稳定的排序。

插入排序,由于插入排序,插入的时候是按照元素的先后顺序插入的(从1 到n - 1),而对于相等的两个元素,直接放在前一个元元素的后面,所以插入排序两个相同元素的前后位置是不会改变的。所以插入排序是稳定的排序。

归并排序,两个相等的元素,拆分和归并之后的相对顺序不会改变。所以是稳定的排序

基数排序和计数排序,两个相等的元素入桶和出桶顺序都是一样的(出入桶相当于队列),不会交换。所以基数排序和计数排序是稳定的排序。

不稳定的排序:

选择排序,由于选择排序对于相邻的两个元素大小相等时,选择前一个为min就不会选择后面的了,这样相对顺序就改变了,

所选择排序不是一个稳定的排序。

希尔排序,由于希尔排序是按照不同间隔对元素进行插入排序(插入排序元素之间的间隔为1,希尔排序间隔是k逐渐递减)。

所以,即使两个元素相邻,但是由于他们在不同的间隔里面,会在各自的间隔里面随机移动。所以希尔排序不稳定。

快速排序,快速排序的不稳定发生在key和中间元素交换的时候,例如序列为3 4 3 8  6 8 7 9 5,5会和第一个8交换,所以破坏了稳定性。所以快速排序不稳定。

堆排序,在排序时如果parrent的两个子节点相等,在堆排时,会将左孩子调整到堆顶,这样就破坏了他们的相对位置。所以堆排序不稳定。


排序算法的对比:

算法练习-9种排序算法_第1张图片

常用排序算法思路及其实现:

  1. 冒泡排序
  2. 选择排序
  3. 插入排序
  4. 希尔排序
  5. 堆排序
  6. 快速排序
  7. 归并排序
  8. 计数排序
  9. 基数排序


以上算法的实现:

冒泡排序:

外层循环控制比较层数,内层循环控制比较次数。使用一个finish来标记是否已经有序了。

还可以优化的是,每次从左向右找都找到最小的,和最大的,最小的放到第一个位置,最大的放到最后一个位置。

public static void bubbleSort(int[] arr) {
		for(int i = 0, len = arr.length; i < len; ++i) {
			boolean finish = true;
			for(int j = 0; j < len - i - 1; ++j) {
				if(arr[j] > arr[j + 1]) {
					finish = false;
					Util.swap(arr, j, j + 1);
				}
			}
			if(finish)
				break;
		}
	}

选择排序:

每次都从序列中选一个最大的和一个最小的,然后将最大的和‘最后一个’交换,最小的和‘第一个’交换。

特例是当min为’最后一个元素‘时,先交换max会将min所指向的元素换走。或者先交换min的时候,max为‘第一个’元素,

public static void selectSort(int[] arr) {
		int len = arr.length;
		int max = 0;
		int min = 0;
		for(int i = len - 1; i >= 0; --i) {
			min = len - 1 - i;
			max = i;
			
			if(max == min)
				break;
			
			for(int j = len - 1 - i; j <= i; ++j) {
				if(arr[j] > arr[max])
					max = j;
				if(arr[j] < arr[min])
					min = j;
			}
			
			if(min != len - 1 - i) {
				Util.swap(arr, min, len - 1 - i);
				if(max == len - 1 - i)
					max = min;
			}
			
			if(max != i) {
				Util.swap(arr, max, i);
			}
		}
	}

插入排序:

第一个元素默认有序,然后从第二个元素起,开始向前比较如果遇到的元素比这个元素小,就将这个元素插到遇到的元素后面

这个函数中gap是1。设置gap是用来给shell排序用的。

	public static void insertSort(int[] arr, int gap) {
		int len = arr.length;
		for(int i = gap; i < len; i += gap) {
			int key = arr[i];
			while(i - gap >= 0 && arr[i - gap] > key) {
				arr[i] = arr[i - gap];
				i -= gap;
			}
			arr[i] = key;
		}
	}

希尔排序(shell排序):

希尔排序是插入排序的变种,设置一个间隔,然后对间隔这么多的元素进行排序。然后这个间隔逐渐减小,直到减少到0。

// 希尔排序,gap是变化的
	public static void shellSort(int[] arr) {
		int len = arr.length;
		for(int gap = len/2 + 1; gap > 0; --gap) {
			insertSort(arr, gap);
		}
	}

堆排序:

什么是堆?

利用堆顶元素为最大或者最小的特点,每次将堆顶元素和最后一个元素交换,然后对这n - 1个元素进行向下调整。继续进行

第一个元素和倒数第二个元素进行交换,然后对着n - 2个元素进行向下调整,直到n  - 1== 0结束排序。升序使用大堆,降序使用小堆。

建堆:

// 建堆
	public static void buildHeap(int[] arr) {
		int len = arr.length;
		int parrent = (len - 2) >> 1; //第一个非叶子节点
		while(parrent >= 0) {
			adapter(arr, len, parrent);
			--parrent;
		}
	}

向下调整:

// 向下调整, len为开区间
	public static void adapter(int[] arr, int len, int parrent) {
		int child = 0;
		while(true) {
			child = parrent * 2 + 1;
			if(child >= len)
				break;
			// 找最大的孩子
			if(child + 1 < len && arr[child + 1] > arr[child])
				++child;
			// 判断最大的孩子是否大于parrent
			if(arr[parrent] < arr[child]) {
				Util.swap(arr, parrent, child);
				parrent = child;
			} else {
				break;
			}
		}
	}

开始排序:

// 堆排序
	public static void heapSort(int[] arr) {
		buildHeap(arr);
		for(int i = arr.length - 1; i > 0; --i) {
			Util.swap(arr, 0, i);
			adapter(arr, i, 0); 
		}
	}

快速排序:

快速排序有三种实现方法,但是步骤只有三步。

第一步为快排的核心(分组),将序列分为两组,左边的比序列大,右边的比序列小。

第二步,对左边再进行第一步。

第三步,对右边再进行第一步。

对于第一步有三种实现方法。

这里以升序为例

方法1:

最常见的方法:内聚法,先随便设置一个元素为key(例如right所在位置的元素),然后在当前待排序列的最左边设置一个begin标记,最右边设置一个end标记,begin++如果左边遇到了比key大的,停下,然后end--,如果右边遇到了比key小的。交换begin和end所指向的元素,直到begin>= end。退出此次排序,然后交换key和begin所在位置的元素,此时begin和end一定是相等的。

以下是实现。

/**
	 * 聚合法,同时从两端开始找,左边找到大于key的,右边找到小于key的,然后交换这两个元素
	 * @param arr 当前待排数组
	 * @param left 
	 * @param right
	 * @return
	 */
	public static int split3(int[] arr, int left, int right) {
		int begin = left;
		int end = right - 1;
		int key = arr[end];
		while(begin < end) {
			// 向后找比key大的
			while(begin < end && arr[begin] <= key) {
				++begin;
			}
			// 向前找比key小的
			while(begin < end && arr[end] >= key) {
				--end;
			}
			// 交换两个找到的元素
			if(begin < end)
				Util.swap(arr, begin, end);
		}
		Util.swap(arr, begin, right - 1); // 将标号所在元素居中
		return begin;
	}

方法2:

挖坑法:

和内聚法一样,不同的是,挖坑法是先将key所在位置设置为坑,然后左边设置一个begin标记,右边设置一个end标记。如果左边遇到了比key大的,就将左边这个元素放到坑,里面,然后left的当前位置设置为坑,然后end--,如果遇到了比key小的,就放到坑里面,然后将当前位置设置为坑。一直循环,直到begin == end;最后将key赋值给最后一个坑,就是begin所在的位置。

/**
	 * 挖坑法
	 * 先将right所在位置设置为坑,并记录他的元素大小,作为一个分界(key),
	 * 然后left向右找,如果遇到了比key大的,则把当前元素赋值给坑,并把当前位置设置为坑,然后right向左找,如果遇到了
	 * 比key小的,就把当前元素赋值给坑,然后把当前位置设置为坑。
	 */
	public static int split2(int[] arr, int left, int right) {
		int begin = left;
		int end = right - 1;
		int key = arr[end];
		while(begin < end) {
			
			// 向后找
			while(begin < end && key > arr[begin]) {
				++begin;
			}
			
			// 当前位置设置为坑,然后把坑填住,并不再访问这个元素。
			if(begin < end) {
				arr[end] = arr[begin];
				--end;
			}
			
			// 向前找
			while(begin < end && key < arr[end]) {
				--end;
			}
			
			// 当前位置设置为坑,然后把坑填住,并不再访问这个元素。
			if(begin < end) {
				arr[begin] = arr[end];
				++begin;
			}
		}
		
		arr[begin] = key;
		
		return begin;
	}

方法3:

前后标记法:

设置两个标记,一个cur标记表示当前位置,一个pre总是标记大于key的前一个小于key的元素。

/**
	 * 前后指针法,cur是当前指针,pre总是指向小于key的元素,pre的下一个一定是大于key的元素,cur一直向后找,如果遇到比key小的,
	 * ++cur, ++pre
	 * 如果遇到比key大的,就只是cur++,pre不变,如果cur再次遇到比key小的,如果pre不等于cur,就交换pre的下一个元素和cur
	 * @param arr
	 * @param left
	 * @param right
	 */
	public static int split1(int[] arr, int left, int right) {
		int cur = left;
		int pre = left - 1;
		int key = arr[right - 1];
		while(cur < right) {
			// 看当前元素是否大于
			if(arr[cur] < key && ++pre != cur) {
				Util.swap(arr, cur, pre);
			}
			++cur;
		}
		
		// 交换pre的下一个和key
		if(++pre < right) {
			Util.swap(arr, pre, right - 1);
		}
		return pre;
	}

对上述方法的调用:

public static void quickSort(int[] arr, int left, int right) {
		// 大于一个元素才排序
		if(right - left > 1) {
			int mid = split3(arr, left, right);
			quickSort(arr, left, mid);
			quickSort(arr, mid + 1, right);
		}
	}

使用挖坑法和内聚法时快排最后总是有left == right,所以最终是使用left或者right作为中间元素赋值其实都是一样的。

对于快速排序的优化:快速排序如果想要排的是顺序,但是元素是倒序的,时间复杂度就会下降到O(n^2),所以每次对于key的选择是非常重要的,如果每次都选择最小的元素的话,每次调整都没有效果,所以对于快速排序的优化主要是对找key元素的优化。取三个元素left mid right, 三个位置的元素,取他们中大小居中的元素。这样每次取到最小元素或者最大元素的概率就会变小。

归并排序:

使用递归将元素拆分为n个1个元素的序列,再将每一对序列(前后两个元素)按照归并的规则合并。继续归并一对元素都是2个已排好序的序列。

public class Test {
	public static void mergeSort(int[] arr) {
		int len = arr.length;
		int left = 0, right = len;
		int[] tmp = new int[len];
		_mergeSort(arr, left, right, tmp);
	}
	
	/**
	 * 对数据进行分组
	 * @param arr 数据
	 * @param left
	 * @param right
	 * @param tmp 临时数组
	 */
	public static void _mergeSort(int[] arr, int left, int right, int[] tmp) {
		if(right - left > 1) {
			// 先求mid
			int mid = left + ((right - left) >> 1);
			// 将左边的数据进行分组
			_mergeSort(arr, left, mid, tmp);
			// 将右边的数据进行分组
			_mergeSort(arr, mid, right, tmp);
			// 合并左右两个分好组的数据
			mergeData(arr, left, mid, right, tmp);
			// 将tmp数组拷贝给arr
			copyArray(tmp, arr, left, right);
		}
	}
	
	/**
	 * 对两区间之间的数据进行合并,然后放到tmp数组里面去
	 * @param arr 待排序列
	 * @param left 左区间的左边界
	 * @param mid 左区间的右边界,不包含,作为右区间的左边界时包含
	 * @param right 右区间的右边界
	 * @param tmp 存放当前排好的序列。
	 */
	public static void mergeData(int[] arr, int left, int mid, int right, int[] tmp) {
		int len = tmp.length;
		int i = left;
		int cur = mid;
		while(i < len) {
			if (arr[left] <= arr[cur]) {
				tmp[i++] = arr[left++];
			} else {
				tmp[i++] = arr[cur++];
			}
			
			if(left == mid) {
				while(cur < right) {
					tmp[i++] = arr[cur++];
				}
				break;
			}
			if(cur == right) {
				while(left < mid) {
					tmp[i++] = arr[left++];
				}
				break;
			}
			
		}
	}
}
	

归并排序的非递归实现:

省去了拆分的步骤,直接从最后一个元素开始归并。

// 非递归版本的归并排序
	public static void mergeSortNor(int[] arr) {
		// 归并间隔
		int gap = 1;
		int len = arr.length;
		int[] tmp = new int[len];
		while(gap < len) {
			for(int i = 0; i < len; i += (2*gap)) {
				int left = i;
				int right = i + gap;
				if(right > len)
					right = len;
				int mid = left + ((right - left) >> 1);
				mergeData(arr, left, mid, right, tmp);
				copyArray(tmp, arr, left, right);
				Util.printArr(tmp);
			}
			gap *= 2;
		}
		
	}

计数排序:

适合排已知数据范围的(min, max)

将数据放到对应的桶中,桶和元素的值是一一对应的,所以只要遍历数组中存的元素个数就可以知道对应的序列

11 15 12 13 21 25

len = max - min + 1= 15

                  11 12 13  15  21  25

对应的桶号为0   1   2   4   10   14

排序后序列:11 12 13 15 21 25

public class CountSort{
	/**
	 * 计数排序
	 * @param arr
	 */
	public static void countSort(int[] arr) {
		int len = arr.length;
		int maxValue = arr[0];
		int minValue = arr[0];
		
		// 找到区间范围
		for(int i = 1; i < len; ++i) {
			if(arr[i] > maxValue) {
				maxValue = arr[i];
			}
			if(arr[i] < minValue) {
				minValue = arr[i];
			}
		}
		
		// 申请计数空间,计数空间大小
		int proxyLen = maxValue - minValue;
		int[] proxy = new int[proxyLen + 1];
		
		// 计数
		for(int i = 0; i < len; ++i) {
			proxy[arr[i] - minValue]++;
		}
		
		// 拷贝回原数组,拷贝次数为原数组元素个数次
		for(int j = 0, i = 0; j < len; ++i) {
			int count = proxy[i];
			while(count-- > 0) {
				arr[j++] = i + minValue;
			}
		}
	}
}

基数排序:

原理:主要是对当前位数相同的时候的处理,比如52, 53 54这三个元素在第一次处理个位之后,第二次处理10位的时候顺序就不变了,同理百位。

方法1:LSD,从低位开始排

方法2:LMD,从高位开始排

LSD的步骤:

1. 获取最大数据的位数。

2. LSD: 从低位开始排,个位 -> 十位 -> 百位

3. 每次排好之后就将结果拷贝回原数组

/**
 * 基数排序
 * @author Z7M-SL7D2
 */
public class RadixSort {
	
	private RadixSort() {}
	
	/**
	 * 基数排序,LSD,从低位向高位排
	 * @param arr
	 */
	public static void radixSort(int[] arr){
		int len = arr.length;
		
		if(len <2)
			return;
		
		// 求出位数
		int digit = getDigit(arr);
		// 用于存放出现的次数
		int[] proxy = new int[10];
		// 用于存放起始地址
		int[] startAddr = new int[10];
		// 临时存放排好序的数组
		int[] tmp = new int[len];
		// 倍数
		int times = 1;
		
		for(int k= 0; k < digit; ++k) {
			// 先求出每个桶里面有多少个元素
			for(int i = 0; i < len; ++i) {
				proxy[(arr[i] / times) % 10]++;
			}
			
			// 求出每个元素在数组中对应的起始地址
			int sum = 0;
			for(int i = 1; i < 10; ++i) {
				sum += proxy[i - 1];
				startAddr[i] = sum;
			}
			
			// 给临时数组赋值
			for(int i = 0; i < len; ++i) {
				int pos = ((arr[i] / times) % 10);
				tmp[startAddr[pos]++] = arr[i];
			}
			
			// 将此次排好序的数组拷贝回原数组。
			Util.copyArr(tmp, arr);
			// 将地址数组清空
			Util.clearArr(startAddr);
			// 存个数的数组清空
			Util.clearArr(proxy);
			Util.printArr(tmp);
			times *= 10;
		}
	}
	
	/**
	 * 用来获取数组最大位数的函数
	 * @param arr
	 * @return
	 */
	public static int getDigit(int[] arr) {
		int len = arr.length;
		int count = 1;
		int times = 10;
		for(int i = 0; i  times) {
				times *= 10;
				++count;
			}
		}
		
		return count;
	}
}	

基数排序每次排完之后的结果:

算法练习-9种排序算法_第2张图片









你可能感兴趣的:(算法练习)