一、插入排序
1.1 直接插入排序
直接插入排序算法的原理如下
插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程。
public class InsertionSort {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2};
int i,j,temp;
for( i=1;i< arr.length;i++){
if(arr[i]<arr[i-1]){ //前有序表的最后一个元素
// 大于后一个无序表中元素,要把此元素插入到前面有序表,使其依然有序
temp=arr[i]; // 后续需要移位,故A[i]会被破坏,要暂存A[i]
for (j=i-1;j>=0 && arr[j]>temp;j--){ //有序表从后往前遍历
arr[j+1]= arr[j]; // 所有大于temp的元素都后移一位
}
arr[j+1]=temp; // 复制到插入位置
// 如果在for循环中声明j,此操作就会出错
}
}
System.out.println(Arrays.toString(arr));
}
}
1.2 直接插入排序(优化 利用折半查找-找到要插入的位置)
public class BInsertionSort {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2};
int i,j,temp,low,high,mid;
for( i=1;i< arr.length;i++){
if(arr[i]<arr[i-1]){ //前有序表的最后一个元素
// 大于后一个无序表中元素,要把此元素插入到前面有序表,使其依然有序
temp=arr[i]; // 后续需要移位,故A[i]会被破坏,要暂存A[i]
low=0;
high=arr.length-1;
// 最后要查找后 指针 high在low左边一位
while (low<=high){ // 找到要插入的位置,[low,i-1]全部后移
mid=(low+high)/2;
if (arr[mid]<temp){
low=mid+1;
}else {
high=mid-1;
}
}
for (j=i-1;j>=low ;j--){ //有序表从后往前遍历 low==high+1
arr[j+1]= arr[j]; // 所有大于temp的元素都后移一位
}
arr[j+1]=temp; // 复制到插入位置
// 如果在for循环中声明j,此操作就会出错
}
}
System.out.println(Arrays.toString(arr));
}
}
2 希尔排序
(先追求局部有序,再逼近全局有序—增量d不断变小)
希尔排序算法的原理如下:
先将待排序表分割成若干形如 L [ i,i+d,i+2d,…,i+kd ]的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止。
2.1 交换法 (先分组后冒泡)
此方案比直插还慢
代码如下:
public class ShellSort {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2,5,6,2,5,7,8};
int i,j,d,temp; // d表示步长
for(d=arr.length/2;d>=1;d/=2){ // 初始的步长为数组的一半,后面不断减半
for (i=d;i< arr.length;i++){ //d+1从子表的第二个开始比较插入
// 遍历各组中所有的元素,步长为d
for (j=i-d;j>=0 ;j-=d){
// 如果当前元素大于加上步长后的那个元素,说明交换
if(arr[j]>arr[j+d]){
temp=arr[j];
arr[j]=arr[j+d];
arr[j+d]=temp;
}
}
}
}
System.out.println(Arrays.toString(arr));
}
}
2.2 移位法 (先分组后直插)
代码如下:
public class ShellSort {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2,5,6,2,5,7,8};
int i,j,d,temp; // d表示步长
for(d=arr.length/2;d>=1;d/=2){ // 初始的步长为数组的一半,后面不断减半
for (i=d;i< arr.length;i++){ //d+1从子表的第二个开始比较插入
if (arr[i]<arr[i-d]){ // i++是不断切换子表操作
temp=arr[i]; // 直接插入排序
for (j=i-d;j>=0 && arr[j]>temp;j-=d){
arr[j+d]=arr[j];
}
arr[j+d]=temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
}
二、交换排序
1、冒泡排序
(每趟中两两对比-可能交换,把最值放到一端)
冒泡排序算法的原理如下:
1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2、对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3、针对所有的元素重复以上的步骤,除了最后一(随趟数增加每趟找到的最大或最小不再参与排序)个。
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
5、优化:通过表示变量flag,在进入交换后改变其值,如果flag值没有改变则标志没有数据交换(已排序好)
含标识变量优化
public class BubbleSort {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2};
int temp=0; // 临时变量
boolean flag=false; // 标识变量,表示是否进行过交换
for(int i= arr.length-1;i>=0;i--){
for (int j=0;j< i;j++){
// 如果前面比后面的小则交换
if(arr[j]<arr[j+1]){
flag=true; // 进入过交换则把标识变量赋值为true
temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
if(flag==false){ // 在此趟排序中一次交换都没有发生过
break;
}else {
flag=false; // 如果交换,重置flag,进行下趟判断是否交换
}
}
System.out.println(Arrays.toString(arr));
}
}
2、快速排序(分治策略)
双指针快速排序基本原理:
1、设置两个变量 low、high,排序开始时:low=0,high=size-1。
2、整个数组找基准正确位置,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面
2.1)默认数组的第一个数为基准数据,赋值给key,即key=array[low]。
2.2)因为默认数组的第一个数为基准,所以从后面开始向前搜索(high–),找到第一个小于key的array[high],就将 array[high] 赋给 array[low],即 array[low] = array[high]。(循环条件是 array[high] >= key;结束时 array[high] < key)
2.3)此时从前面开始向后搜索(low++),找到第一个大于key的array[low],就将 array[low] 赋给 array[high],即 array[high] = array[low]。(循环条件是 array[low] <= key;结束时 array[low] > key)
2.4)循环 2-3 步骤,直到 low=high,该位置就是基准位置。
2.5)把基准数据赋给当前位置。
3、第一趟找到的基准位置,作为下一趟的分界点。
4、递归调用(recursive)分界点前和分界点后的子数组排序,重复2.2、2.3、2.4的步骤。
5、最终就会得到排序好的数组。
public class QuickSort {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2,5,6,2,5,7,8};
Quicksort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void Quicksort(int arr[],int low,int high){
if (low<high){
int keypos=Partition(arr,low,high); //获取划分最后key的索引位置
Quicksort(arr,low,keypos-1); // 递归划分key索引左表
Quicksort(arr,keypos+1,high); // 递归划分key索引右表
}
}
public static int Partition(int arr[],int low ,int high){
int key=arr[low]; // 保存第一个元素作为基准
while (low<high){
while (low<high && arr[high]>=key){
high--;
}
// 出现比key小的数,将其移到左边
arr[low]=arr[high]; //因为arr[low]已经保存下来,可以被覆盖
// 左右两边来回切换
while (low<high && arr[low]<key){
low++;
}
// 出现比key大的数,将其移到右边
arr[high]=arr[low]; //因为arr[high]已经被上述arr[low]保存下来,
// 可以被覆盖
}
// 大循环的停止时是 --- low==high
arr[low]=key; // 把基准赋给到最终low/high的位置
return low; // 返回当前位置,以便后面递归分组
}
}
单指针快速排序基本原理:
出现小的移动指针,大的则不移动,故当出现小的时,mark移动到大于基准的索引位置,然后与现在的值arr[i]交换,使得小的值移到前面,大的值移到后面,最后基准与mark位置交换,并返回mark指针对应的索引位置,以便后续递归划分左右表。
public class QuickSort {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2,5,6,2,5,7,8};
Quicksort2(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void Quicksort2(int arr[],int low,int high){
if (low<high){
int keypos=partition2(arr,low,high); //获取划分最后key的索引位置
Quicksort2(arr,low,keypos-1); // 递归划分key索引左表
Quicksort2(arr,keypos+1,high); // 递归划分key索引右表
}
}
// 单边循环 单指针快排
public static int partition2(int[] arr ,int low ,int high){
int pivot = arr[low]; // 取第一个元素为基准
int mark = low;
for (int i=low+1;i<=high;i++){
if(arr[i]<pivot){ // 出现小的移动指针,大的则不移动,故当出现小的时,mark移动到大于基准的索引位置,然后与现在的值arr[i]交换,使得小的值移到前面,大的值移到后面,最后基准与mark位置交换,并返回mark指针。
mark++;
int temp=arr[mark];
arr[mark]=arr[i];
arr[i]=temp;
}
}
// mark与一开始基准元素交换,确定新基准
arr[low]=arr[mark];
arr[mark]=pivot;
return mark;
}
三、选择排序
1、简单选择排序
(每趟对比起始位置和后续的值,得到后续中最小的值与起始位置交换【可能最小的就是起始位】)
选择排序算法的原理如下:
工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
1、选择排序一共有数组大小-1论排序
2、每一轮排序,又是一个循环,循环的规则(代码)
2.1 先假定当前这个数是最小数
2.2 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标
2.3 当遍历到数组的最后时,就得到本轮最小数和下标
2.4 交换当前数和最小数(代码)
重复以上操作
public class SelectionSort {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2};
for (int i=0 ;i<arr.length;i++){
int min=i; // 临时变量 存放临时最小值的的索引
for(int j=i+1;j< arr.length;j++){ // 从当前数的下一个开始遍历
if(arr[j]<arr[min]){ // 遍历得到最小值的索引
min=j;
}
}
if(min!=i){
swap(arr,i,min); //如果遍历后 最小值的索引改变,
// 则交换当前i索引对应的值和遍历得到最小值索引的最小值
}
}
System.out.println(Arrays.toString(arr));
}
public static void swap(int arr[],int i,int min){
int temp =arr[i];
arr[i]=arr[min];
arr[min]=temp;
}
}
2、堆排序
堆的概述:
若n个关键字序列L[1…n] 满足下面某一条性质,则成为堆(Heap):
① 若满足:L(i) >= L(2i) 且 L(i) >= L(2i+1) (1 <= i <= n/2)—大根堆(大顶堆)
② 若满足:L(i) <= L(2i) 且 L(i) <= L(2i+1) (1 <= i <= n/2)—小根堆(小顶堆)
建立大根堆:
思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整。如果小于左右孩子,把两个中更大的与其交换
注意:当前结点处理后,更小的元素下坠,可能导致下一层子树不符合大根堆的要求,故需要继续往下调整。
在顺序存储的完全二叉树中,非终端结点编号i<=n/2 —向下取整。
建立大根堆(代码)
// 建立大根堆
public static void BuildMaxHeap(int arr[],int len){
// arr元素索引是从0开始的,所以最后一个非终端结点len/2-1
// ---即arr.length/2-1
for(int i=len/2-1;i>=0;i--){ // 从后往前遍历调整所有非终端结点
HeadAdjust(arr,i,len); //调整堆
}
}
// 把数组调整为大根堆
// k代表非终端结点 ,len代表树中元素的个数(数组的长度)
public static void HeadAdjust(int arr[],int i,int len){
int temp=arr[i]; // temp暂存子树的根结点,后续可能被交换
for (int k=2*i+1;k<len;k=2*k+1){
// //2*i+1为左子树i的左子树(因为i是从0开始的),2*k+1为k的左子树
// 沿值较大的子结点向下筛选
if (k+1<len && arr[k]<arr[k+1]){ //如果有右子树,并且右子树大于左子树
k++; // 取到当前非终端结点的左右孩子值最大对应的索引
}
if (temp>=arr[k]){
break; // 根结点比左右孩子中最大的都大,不用交换,结束循环
}else { // 如果小于最大的就要交换
arr[i]=arr[k]; // 把当前结点的左右孩子中最大的赋给当前结点
i=k; // 当前结点的值下坠,可能导致下层不满足大根堆,
// 因此从被交换的孩子索引(交换后当前结点对应的位置),继续向下筛选
}
}
arr[i]=temp; // 最后把当前结点的值赋给最后k停留的位置
}
堆排序:(代码如下)
每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换),并将待排序元素序列再次调整为大根堆(小元素不断“下坠”)
public static void Heapsort(int arr[],int len){
BuildMaxHeap(arr,len); // 初始建堆
for (int i=len-1;i>0;i--){ // n-1趟交换和建堆过程
swap(arr,0,i); // 堆顶元素索引0和堆底元素交换
HeadAdjust(arr,0,i); // 把剩余的待排序元素整理成大根堆
// 索引0位置的最大值被换到i(len-1),剩余[0,len-1]调整为大根堆
}
}
/**
* 交换元素
* @param arr
* @param a 元素的下标
* @param b 元素的下标
*/
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
四、归并排序
分治策略,字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
归并操作的工作原理如下:
1、创建一个辅助数组,使其大小为原序列的长度,该空间用来存放合并后的序列
2、递归划分左右序列–直到每个左右只有一个元素
3、执行合并操作
3.1)设定两个指针i、j,最初位置分别为两个序列的起始位置low、mid+1
3.2)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3.2直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
代码如下:
public class MergeSort {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2,5,6,2,5,7,8};
int[] temp = new int[arr.length]; //创建辅助数组
Mergesort(arr,0,arr.length-1,temp);
System.out.println(Arrays.toString(arr));
}
public static void Mergesort(int arr[],int low,int high,int temp[]){
if (low<high){
int mid=(low+high)/2; // 从中间划分
Mergesort(arr,low,mid,temp); // 递归对左半部分划分归并排序
Mergesort(arr,mid+1,high,temp); // 递归对右半部分划分归并排序
Merge(arr,low,mid,high,temp); // 把当前划分后的元素,归并排序
}
}
/*
* arr:排序的原始数组
* low:初始索引
* mid:中间索引
* high:最后索引
* temp:中转数组
* */
public static void Merge(int arr[] ,int low,int mid ,int high,int temp[]){
int i,j,k; // i(low):左边有序序列的初始索引
// j(mid+1):右边有序序列的初始索引
// k:指向temp数组的当前索引
for (k=low;k<=high;k++){ //把原数组的元素复制到temp数组中
temp[k]=arr[k];
}
for (i=low,j=mid+1,k=i;i<=mid && j<=high ;k++){
if (temp[i]<temp[j]){
arr[k]=temp[i++]; // 将较小的值重新赋给arr数组
}else {
arr[k]=temp[j++];
}
}
// for循环结束是由于其中一个子序列归并完,故需要把另一个剩余部分归并到尾部
while (i<=mid) arr[k++]=temp[i++];
while (j<=high) arr[k++]=temp[j++];
}
}
五、基数排序
基数排序是效率高的稳定性的排序,是桶排序的扩展。
基数排序基本思想
将所有待比较数值统一为同样的数位长度,数位较短的前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。(LSD—最低为优先)
1)首先要得到最大数,确定最大数是几位数
2)定义一个二维数组,表示10个桶(每位取值范围0-9),每个桶就是一个一维数组,同时要定义一个一维数组来记录每个桶中存放数据的个数。在每轮大循环过后,都需要把其置为0
3)通过循环从个位到百位放入桶中,再依桶的顺序取出存放再每个桶中的数据,存到原始数组中(覆盖)。
特别注意:每一轮处理后,要把每个桶中存放数据的个数置零,bucketElementCounts[k]=0 !!!
public class RadixSort {
public static void main(String[] args) {
int arr[]={53,3,542,748,14,214};
radixSort(arr);
System.out.println(Arrays.toString(arr));
}
// 基数排序
public static void radixSort(int[] arr){
// 1、得到数组中最大数的位数
int max=arr[0]; // 假定第一个数就是最大的数
for (int i=1;i< arr.length;i++){
if (arr[i]>max){
max=arr[i];
}
}
// 得到最大数是几位数
int maxLength=(max + "").length();
//定义一个二维数组,表示10个桶,每个桶就是一个一维数组
// 说明
// 1、二维数组包含10个一维数组
// 2、防止在放入数的时候,数据溢出,每个一维数组(桶),大小定为arr.length
// 3、故可以看出,基数排序是使用空间换时间的经典算法
int[][] bucket = new int[10][arr.length];
// 为了记录每个桶中,实际存放多少数据,定义一个一维数组来记录每个桶的数据个数
// 例如 bucketElementCounts[0]记录就是bucket[0]桶中数据个数
int[] bucketElementCounts = new int[10];
// 使用循环处理
for (int i=0,n=1 ;i<maxLength;i++,n*=10){ // 数取模用到 n
// 针对每个元素对应的位进行排序,个、十、百。。。
for (int j=0 ;j< arr.length;j++){
// 去除每个元素的对应位的值
int digitofElement = arr[j] / n % 10;
// 放入到对应的桶中 bucketElementCounts[digitofElement]默认0,即桶下标0存第一个数据
bucket[digitofElement][bucketElementCounts[digitofElement]] = arr[j];
bucketElementCounts[digitofElement]++; // 记录的是每个桶存数据的个数
}
// 按照桶的顺序(一维数组的下标依次取出数据,放入原来数组)
int index = 0;
// 遍历每一个桶,并将桶中数据,放回原数组
for(int k=0 ;k< bucketElementCounts.length;k++){
//如果桶中有数据,我们才放入到原数组
if (bucketElementCounts[k]!=0){ // 不为0,即存入了和数据
//循环该桶即第k个桶(即第k个一维数组)
for (int l=0;l< bucketElementCounts[k];l++){
// 取出元素放入到arr
arr[index++]=bucket[k][l];
}
}
// 每一轮处理后,要把每个桶中存放数据的个数置零,bucketElementCounts[k]=0 !!!
bucketElementCounts[k]=0;
}
}
}
}