算法练习-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的两个子节点相等,在堆排时,会将左孩子调整到堆顶,这样就破坏了他们的相对位置。所以堆排序不稳定。

 

排序算法的对比:

 

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

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

 

以上算法的实现:

冒泡排序:

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

/**
     * 冒泡排序
     * 左边比右边大则交换
     *
     * @param arr
     */
    public static void bubbleSort(int[] arr) {
        for (int i = 0; i < arr.length; ++i) {
            boolean isSorted = true;
            for (int j = 0; j < arr.length - 1; ++j) {
                if (arr[j + 1] < arr[j]) {
                    isSorted = false;
                    swap(arr, j, j + 1);
                }
            }
            if (isSorted) {
                break;
            }
        }
        print(arr);
    }

选择排序:

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

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

/**
     * 选择排序
     * 每次选择一个最大的数和一个最小的数,最大的和最后第一个数交换,最小的数和第一个数交换
     *
     * @param arr
     */
    public static void selectSort(int[] arr) {
        for (int head = 0; head < arr.length; ++head) {
            int tail = arr.length - 1 - head;
            if (head >= tail) {
                break;
            }
            int min = head;
            int max = head;
            for (int j = head; j < arr.length - head; ++j) {
                if (arr[j] > arr[max]) {
                    max = j;
                }
                if (arr[j] < arr[min]) {
                    min = j;
                }
            }

            // 最小数放到头部
            swap(arr, min, head);
            if (head == max) {
                // min交换位置过了head。所以max现在等于min位置的值
                max = min;
            }
            // 最大数放到尾部
            swap(arr, max, tail);
        }
        print(arr);
    }

插入排序:

第一个元素默认有序,然后从第二个元素起,开始向前比较如果遇到的元素比这个元素小,则交换两个元素的位置,或者先搬移元素,再插入元素

   /**
     * 插入排序
     *
     * @param arr
     */
    public static void insertSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > arr[i - 1]) {
                continue;
            }
            for (int j = i; j > 0; --j) {
                // 后面大于前面则交换
                if (arr[j - 1] > arr[j]) {
                    swap(arr, j - 1, j);
                } else {
                    break;
                }
            }
        }
        print(arr);
    }

希尔排序(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个已排好序的序列。

/**
     * 归并排序
     * 时间复杂度N*logN
     * 空间复杂度N
     *
     * @param arr
     */
    public static void mergeSort(int[] arr) {
        split(arr, 0, arr.length - 1, new int[arr.length]);
    }

    private static void split(int[] arr, int start, int end, int[] tmp) {
        if (start >= end) {
            return;
        }
        int center = (end - start) >> 1 + start;
        // 拆分O(logN)
        split(arr, start, center, tmp);
        split(arr, center + 1, end, tmp);
        // 合并 O(N)
        merge(arr, start, center, end, tmp);
    }

    /**
     * 合并
     *
     * @param arr
     * @param start
     * @param center
     * @param end
     * @param tmp
     */
    private static void merge(int[] arr, int start, int center, int end, int[] tmp) {
        int curRight = center + 1;
        int curTmp = start;
        int curLeft = start;
        // 任意选一个区间循环,因为两个区间长度最多相差1,所以选任意一个都没啥区别
        for (; curLeft <= center; ) {
            if (arr[curLeft] <= arr[curRight]) {
                tmp[curTmp++] = arr[curLeft++];
            } else {
                tmp[curTmp++] = arr[curRight++];
                if (curRight > end) {
                    break;
                }
            }
        }

        if (curTmp <= end) {
            // 还没合并完
            int surplusStart;
            int surplusEnd;
            if (curLeft <= center) {
                surplusStart = curLeft;
                surplusEnd = center;
            } else {
                surplusStart = curRight;
                surplusEnd = end;
            }

            for (; surplusStart <= surplusEnd; ++surplusStart) {
                tmp[curTmp++] = arr[surplusStart];
            }
        }

        // 将合并结果拷贝到原数组中
        for (; start <= end; ++start) {
            arr[start] = tmp[start];
        }
        print(arr);
        System.out.println();
    }

    public static void main(String[] args) {
        int[] arr = new int[]{1, 4, 3, 6, 6, 6, 8, 12, 10, 9, 2, 0};
        mergeSort(arr);
    }

归并排序的非递归实现:

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

/**
     * 归并排序非递归实现
     *
     * @param arr
     */
    public static void mergeSortNor(int[] arr) {
        int gap = 2;
        int[] tmp = new int[arr.length];
        while (gap <= arr.length * 2) {
            for (int start = 0; start < arr.length; ) {
                int end = start + gap;
                if (end >= arr.length) {
                    end = arr.length;
                }
                if (start >= end) {
                    break;
                }

                // 求中点
                int center = (end - start) / 2 + start;
                // 这块比较重要
                if (gap > arr.length) {
                    center = gap / 2;
                }

                merge(arr, start, center, end, tmp);
                start = end;
            }
            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 <len; ++i) {
			if(arr[i] > times) {
				times *= 10;
				++count;
			}
		}
		
		return count;
	}
}	

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

 

 

 

 

 

 

 

 

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值