在CS界曾流行这样一种说法:算法不是万能的,但是没有算法是万万不能的。作为一个具有基本素养的计算机科学从业者,可以说这些是必须知道的。从算法的Easy级别入手,当然要从大家口中常说的,八大排序三大查找开始啦~~~
直接插入排序:通常人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了要给插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。插入排序是稳定的。
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
动态效果图
代码实现
/**
* 通过交换进行插入排序,借鉴冒泡排序
*
* @param a
*/
public static void siSort(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = i + 1; j > 0; j--) {
if (a[j] < a[j - 1]) {
int temp = a[j];
a[j] = a[j - 1];
a[j - 1] = temp;
}
}
}
}
/**
* 通过将较大的元素都向右移动而不总是交换两个元素
*
* @param a
*/
public static void siSort2(int[] a) {
for (int i = 1; i < a.length; i++) {
int num = a[i];
int j;
for (j = i; j > 0 && num < a[j - 1]; j--) {
a[j] = a[j - 1];
}
a[j] = num;
}
}
希尔排序,一个叫希尔的人发明的排序算法:希尔排序是按照不同步长对元素进行插入排序,一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,稳定性就会被破坏,所以希尔排序不稳定。
public static void shellSort(int[] a) {
int length = a.length;
int h = 1;
while (h < length / 3) h = 3 * h + 1;
for (; h >= 1; h /= 3) {
for (int i = 0; i < a.length - h; i += h) {
for (int j = i + h; j > 0; j -= h) {
if (a[j] < a[j - h]) {
int temp = a[j];
a[j] = a[j - h];
a[j - h] = temp;
}
}
}
}
}
简单选择排序:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
public static void ssSort(int[] a) {
for (int i = 0; i < a.length; i++) {
int min = i;
//选出之后待排序中值最小的位置
for (int j = i + 1; j < a.length; j++) {
if (a[j] < a[min]) {
min = j;
}
}
//最小值不等于当前值时进行交换
if (min != i) {
int temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
}
堆排序:
堆的定义如下:n个元素的序列{k1,k2,…,kn}
当且仅当满足下关系时,称之为堆。
把此序列对应的二维数组看成一个完全二叉树。那么堆的含义就是:完全二叉树中任何一个非叶子节点的值均不大于(或不小于)其左,右孩子节点的值。 由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。因此我们可使用大顶堆进行升序排序, 使用小顶堆进行降序排序。
先将初始序列K[1…n]建成一个大顶堆, 那么此时第一个元素K1最大, 此堆为初始的无序区.
再将关键字最大的记录K1 (即堆顶, 第一个元素)和无序区的最后一个记录 Kn 交换,
由此得到新的无序区K[1…n−1]和有序区K[n], 且满足K[1…n−1].keys⩽K[n].key
交换K1 和 Kn 后, 堆顶可能违反堆性质, 因此需将K[1…n−1]调整为堆. 然后重复步骤2, 直到无序区只有一个元素时停止。
/**
* @param a
*/
public static void heapSort(int[] a) {
for (int i = a.length - 1; i > 0; i--) {
max_heapify(a, i);
//堆顶元素(第一个元素)与Kn交换
int temp = a[0];
a[0] = a[i];
a[i] = temp;
}
}
/***
*
* 将数组堆化
* i = 第一个非叶子节点。
* 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
* 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
*
* @param a
* @param n
*/
public static void max_heapify(int[] a, int n) {
int child;
for (int i = (n - 1) / 2; i >= 0; i--) {
//左子节点位置
child = 2 * i + 1;
//右子节点存在且大于左子节点,child变成右子节点
if (child != n && a[child] < a[child + 1]) {
child++;
}
//交换父节点与左右子节点中的最大值
if (a[i] < a[child]) {
int temp = a[i];
a[i] = a[child];
a[child] = temp;
}
}
}
冒泡排序:由前面的内容可知,冒泡排序是相邻的两个元素比较,交换也发生在这两个元素之间,如果两个元素相等,不用交换。所以冒泡排序稳定。
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
效果图
代码实现
public static void sort(int[] a) {
//外层循环控制比较的次数
for (int i = 0; i < a.length - 1; i++) {
//内层循环控制到达位置
for (int j = 0; j < a.length - i - 1; j++) {
//前面的元素比后面大就交换
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
快速排序:快速排序的基本思想:挖坑填数+分治法。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好。
快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为:
从数列中挑出一个元素,称为"基准"(pivot)。
重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
效果图
代码实现
public static void quickSort(int[] a, int low, int high) {
//已经排完
if (low >= high) {
return;
}
int left = low;
int right = high;
//保存基准值
int pivot = a[left];
while (left < right) {
//从后向前找到比基准小的元素
while (left < right && a[right] >= pivot)
right--;
a[left] = a[right];
//从前往后找到比基准大的元素
while (left < right && a[left] <= pivot)
left++;
a[right] = a[left];
}
// 放置基准值,准备分治递归快排
a[left] = pivot;
quickSort(a, low, left - 1);
quickSort(a, left + 1, high);
}
归并排序:并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。归并排序也是稳定的。
归并排序可通过两种方式实现:
一: 递归法(假设序列共有n个元素):
将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素;
将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素;
重复步骤2,直到所有元素排序完毕。
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针到达序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
public class Merge {
//归并所需的辅助数组
private static int[] aux;
public static void sort(int[] a) {
//一次性分配空间
aux = new int[a.length];
sort(a, 0, a.length - 1);
}
public static void sort(int[] a, int low, int high) {
if (low >= high) {
return;
}
int mid = (low + high) / 2;
//将左半边排序
sort(a, low, mid);
//将右半边排序
sort(a, mid + 1, high);
merge(a, low, mid, high);
}
/**
* 该方法先将所有元素复制到aux[]中,然后在归并会a[]中。方法咋归并时(第二个for循环)
* 进行了4个条件判断:
* - 左半边用尽(取右半边的元素)
* - 右半边用尽(取左半边的元素)
* - 右半边的当前元素小于左半边的当前元素(取右半边的元素)
* - 右半边的当前元素大于等于左半边的当前元素(取左半边的元素)
* @param a
* @param low
* @param mid
* @param high
*/
public static void merge(int[] a, int low, int mid, int high) {
//将a[low..mid]和a[mid+1..high]归并
int i = low, j = mid + 1;
for (int k = low; k <= high; k++) {
aux[k] = a[k];
}
for (int k = low; k <= high; k++) {
if (i > mid) {
a[k] = aux[j++];
} else if (j > high) {
a[k] = aux[i++];
} else if (aux[j] < aux[i]) {
a[k] = aux[j++];
} else {
a[k] = aux[i++];
}
}
}
}
基数排序:
基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine), 排序器每次只能看到一个列。它是基于元素值的每个位上的字符来排序的。 对于数字而言就是分别基于个位,十位, 百位或千位等等数字来排序。
具体是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
通过序列中各个元素的值,对排序的N个元素进行若干趟的“分配”与“收集”来实现排序。
分配:我们将L[i]中的元素取出,首先确定其个位上的数字,根据该数字分配到与之序号相同的桶中
收集:当序列中所有的元素都分配到对应的桶中,再按照顺序依次将桶中的元素收集形成新的一个待排序列L[]。对新形成的序列L[]重复执行分配和收集元素中的十位、百位…直到分配完该序列中的最高位,则排序结束
代码实现
public static void radixSort(int[] arr) {
if (arr.length <= 1) return;
//取得数组中的最大数,并取得位数
int max = 0;
for (int i = 0; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
}
int maxDigit = 1;
while (max / 10 > 0) {
maxDigit++;
max = max / 10;
}
//申请一个桶空间
int[][] buckets = new int[10][arr.length];
int base = 10;
//从低位到高位,对每一位遍历,将所有元素分配到桶中
for (int i = 0; i < maxDigit; i++) {
int[] bktLen = new int[10]; //存储各个桶中存储元素的数量
//分配:将所有元素分配到桶中
for (int j = 0; j < arr.length; j++) {
int whichBucket = (arr[j] % base) / (base / 10);
buckets[whichBucket][bktLen[whichBucket]] = arr[j];
bktLen[whichBucket]++;
}
//收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞
int k = 0;
for (int b = 0; b < buckets.length; b++) {
for (int p = 0; p < bktLen[b]; p++) {
arr[k++] = buckets[b][p];
}
}
System.out.println("Sorting: " + Arrays.toString(arr));
base *= 10;
}
}
本文所说的三大查找算法称为三大类的查找算法,分别是:顺序、二分、分块。
其实还有个哈希查找,但是不在本文讨论范围之内
从表的一端开始,顺序扫描表,依次将扫描到的结点关键字和给定值(假定为a)相比较,若当前结点关键字与a相等,则查找成功;若扫描结束后,仍未找到关键字等于a的结点,则查找失败。
说白了就是,从头到尾,一个一个地比,找着相同的就成功,找不到就失败。很明显的缺点就是查找效率低。
适用于线性表的顺序存储结构和链式存储结构。
public static int seqSearch(int[] a, int key) {
for (int i = 0, length = a.length; i < length; i++) {
if (a[i] == key)
return i;
}
return -1;
}
二分法查找适用于大的数据,但前提条件是数据必须是有序的,他的原理是先和中间的比较,如果等于就直接返回,如果小于就在前半部分继续使用二分法进行查找,如果大于则在后半部分继续使用二分法进行查找
public static int binarySearch(int[] array, int value) {
int low = 0;
int high = array.length - 1;
while (low <= high) {
int middle = (low + high) >> 1;
if (value == array[middle]) {
return middle;
}
if (value > array[middle]) {
low = middle + 1;
}
if (value < array[middle]) {
high = middle - 1;
}
}
return -1;
}
分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
算法思想:将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
先选取各块中的最大关键字构成一个索引表;
查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;
然后,在已确定的块中用顺序法进行查找。
public class BlockSearch {
private int[] index;//建立索引
private ArrayList[] list;
/**
* @Descript:初始化索引
*
* @author LJonah 2018年3月12日
* @param index
*/
public BlockSearch(int[] index){
if(null != index && index.length!=0){
this.index = index;
this.list = new ArrayList[index.length];
for (int i = 0;i < list.length;i++) {
list[i] = new ArrayList();//初始化容器
}
}else{
throw new Error("index cannot be null or empty");
}
}
/**
* @Descript:插入索引
*
* @author LJonah 2018年3月12日
* @param value
*/
public void insert(int value){
int i = binarySearch(value);
list[i].add(value);
}
/**
* @Descript:二分法查找
*
* @author LJonah 2018年3月12日
* @param value
* @return
*/
private int binarySearch(int value){
int start = 0,end =index.length;int mid = -1;
while(start<=end){
mid=(start+end)/2;
if(index[mid]>value){
end = mid -1;
}else{
//如果相等,也插入后面
start = mid+1;
}
}
return start;
}
/**
* @Descript:查找元素
*
* @author LJonah 2018年3月12日
* @param data
* @return
*/
public boolean search(int data)
{
int i=binarySearch(data);
for(int j=0;j<list[i].size();j++)
{
if(data==(int)list[i].get(j))
{
System.out.println(String.format("查找元素为第: %d块 第%d个 元素", i+1,j+1));
return true;
}
}
return false;
}
/**
* @Descript:打印每块的元素
*
* @author LJonah 2018年3月12日
*/
public void printAll(){
for (int i = 0; i < list.length; i++) {
ArrayList l = list[i];
System.out.println("ArrayList["+i+"]:");
for (int j = 0; j < l.size(); j++) {
System.out.println(l.get(j)+" ");
}
}
}
/**
* @Descript:测试
*
* @author LJonah 2018年3月12日
* @param args
*/
public static void main(String[] args) {
int []index={
10,20,30};
BlockSearch blocksearch=new BlockSearch(index);
blocksearch.insert(1);
blocksearch.insert(11);
blocksearch.insert(21);
blocksearch.insert(2);
blocksearch.insert(12);
blocksearch.insert(22);
blocksearch.insert(5);
blocksearch.insert(15);
blocksearch.insert(25);
blocksearch.printAll();
System.out.println("查找值15 结果"+blocksearch.search(15));
System.out.println("查找值29 结果"+blocksearch.search(29));
}
}
【1】Java实现八大排序算法
【2】带你彻底搞定希尔排序是个什么情况
【3】分块查询算法(JAVA)