花了一个月时间学习了算法导论上的排序算法,学而不思则惘,现在把学习中的经验心得,都记录下来。
排序
-- 交换排序
-- 冒泡排序 bubble sort
-- 快速排序 quick sort
-- 归并排序
-- 合并排序merge sort
-- 选择排序
-- 选择排序 select sort
-- 堆排序 heap sort
-- 非比较排序
-- 计数排序couting sort
-- 基数排序radix sort
-- 桶排序 bucket sort
-- 插入排序
-- 插入排序inerst sort
-- 其他排序
-- NA
Sortable 接口:
package com.ibm.billyao.sort;
import java.util.List;
public interface Sortable{
/**
* Sort the list by increase order if order equals to 0, or decrease order.
*
* @param list to be sorted list.
* @param order could be 0 or 1 based on the order type.
*/
public void sort(List<Integer> list, int order);
}
冒泡排序
冒泡排序属于比较慢速的排序了,基本思想也很简单,这里就不赘述了。
时间复杂度 O(n^2) 稳定的
空间复杂度 O(1) sort in place
package com.ibm.billyao.sort;
import java.util.ArrayList;
import java.util.List;
public class BubbleSort implements Sortable {
@Override
public void sort(List<Integer> list, int order) {
for(int i = 0; i < list.size(); i++) {
for(int j = list.size() - 1; j > i; j--) {
if(list.get(j) < list.get(j - 1)) {
int tmp = list.get(j);
list.set(j, list.get(j - 1));
list.set(j - 1, tmp);
}
}
}
}
}
快速排序
快速排序是基于冒泡排序的算法的。它的基本思想是,首先选中一个list,然后以list的最后一个值作为基准,可以把这个值放到这样一个位置,使得它的左边的值都比它小,它右边的值都比它大,而且这个算法完全不需要创建新的内存空间(除了几个临时变量)。做法也简单,就是设置两个index,一个指向比他小的值的最大地址,另一个index不停检索,遇到比它小的,就换到前面去。
从快速排序中又延伸出平衡快速排序,随机快速排序等方法,其实都是为了解决在快速排序中的最差情况。但在实际应用做,快速排序可以说速度还是不错的。
时间复杂度 O(n * lgn)
空间复杂度 O(1) sort in place
package com.ibm.billyao.sort;
import java.util.List;
public class QuickSort implements Sortable {
@Override
public void sort(List<Integer> list, int order) {
quickSort(list, 0, list.size() - 1);
}
public void quickSort(List<Integer> list, int p, int r) {
if(p < r) {
int q = partition(list, p, r);
quickSort(list, p, q - 1);
quickSort(list, q + 1, r);
}
}
private int partition(List<Integer> list, int p, int r) {
int val = list.get(r);
int index = p - 1;
for(int j = p; j < r -1; j++) {
if(list.get(j) <= val) {
index++;
exchange(list, index, j);
}
}
return index;
}
/*
private int randomizePartition(List<Integer> list, int p, int r) {
Random random = new Random();
int index = random.nextInt(p - r + 1) + p;
exchange(list, index, r);
return partition(list, p, r);
}
private int balancePartition(List<Integer> list, int p, int r) {
int head = list.get(p);
int middle = list.get((p + r)/2);
int tail = list.get(r);
int index = head;
//TODO: get the middle value in these three.
// exchange with r.
return partition(list, p, r);
}*/
private void exchange(List<Integer> list, int p, int r) {
int tmp = list.get(p);
list.set(p, list.get(r));
list.set(r, tmp);
}
}
合并排序
合并排序的基本思路就是,把要排序的列表先分成两块,分别排序。然后再将之合并。很好的用到了递归的思想。
时间复杂度: O(n * lgn)
空间复杂度: 太复杂,不会算。。。
package com.ibm.billyao.sort;
import java.util.ArrayList;
import java.util.List;
import com.ibm.billyao.search.BinarySearch;
import com.ibm.billyao.search.Searchable;
public class MergeSort implements Sortable {
@Override
public void sort(List<Integer> list, int order) {
if(list == null || list.size() == 0) return;
// Order not supported in this method.
mergeSort(list, 0, list.size() - 1);
}
private void mergeSort(List<Integer> list, int p, int r) {
if(p < r) {
int q = (p + r) / 2;
mergeSort(list, p, q);
mergeSort(list, q + 1, r);
merge(list, p, q , r);
}
}
// Merge...
private void merge(List<Integer> list, int p, int q, int r) {
//Initial helper list.
int nLeft = q - p + 1;
int nRight = r - q;
List<Integer> left = new ArrayList<Integer>();
List<Integer> right = new ArrayList<Integer>();
for(int i = 0; i < nLeft; i++) {
left.add(list.get(p + i));
}
for(int j = 0; j < nRight; j++) {
right.add(list.get(q + j + 1));
}
int i = 0;
int j = 0;
for(int k = p; k < r + 1; k++) {
if(i >= left.size()) {
for(; j < right.size(); j++, k++) {
list.set(k, right.get(j));
}
break;
} else if (j >= right.size()) {
for(; i < left.size(); i++, k++) {
list.set(k, left.get(i));
}
break;
} else if(left.get(i) <= right.get(j)) {
list.set(k, left.get(i));
i++;
} else {
list.set(k, right.get(j));
j++;
}
}
}
}
选择排序
选择排序是最简单也最常见的排序方法,也是最慢的,不推荐使用
时间复杂度: O(n^2)
空间复杂度: O(1)
代码暂无
堆排序
基于选择排序算法,引入堆(heap)的概念,通过使用最大堆来实现排序。
时间复杂度: O(n* lgn)
空间复杂度: O(n)
package com.ibm.billyao.sort;
import java.util.ArrayList;
import java.util.List;
public class HeapSort implements Sortable {
@Override
public void sort(List<Integer> list, int order) {
Heap heap = new Heap(list);
heap.print();
List<Integer> result = new ArrayList<Integer>();
while(heap.getHeapSize() > 0) {
result.add(heap.pop());
}
System.out.println("");
for(int val : result) {
System.out.print(" " + val);
}
}
class Heap {
List<Integer> storage = null;
public Heap() {
storage = new ArrayList<Integer>();
}
public Heap(List<Integer> storage) {
this.storage = storage;
this.build();
}
public int getHeapSize() {
return storage.size();
}
public int getParent(int pos) {
return pos / 2;
}
public int getLeft(int pos) {
if(getHeapSize() > (2* pos))
return pos * 2;
return -1;
}
public int getRight(int pos) {
if(getHeapSize() > (2* pos + 1))
return pos * 2 + 1;
return -1;
}
public int pop() {
int result = storage.get(0);
storage.remove(0);
build();
return result;
}
private void build() {
for(int i = storage.size() / 2; i >= 0; i--) {
maxHeapify(i);
}
}
protected void maxHeapify(int pos) {
int l = getLeft(pos);
int r = getRight(pos);
int largest = -1;
if(l != -1 && storage.get(l) >= storage.get(pos)) {
largest = l;
} else {
largest = pos;
}
if(r != -1 && storage.get(r) >= storage.get(largest)) {
largest = r;
}
if(largest != pos) {
int tmp = storage.get(pos);
storage.set(pos, storage.get(largest));
storage.set(largest, tmp);
maxHeapify(largest);
}
}
public void print() {
for(int node : storage) {
System.out.print(" " + node);
}
}
}
class PQueen extends Heap {
public int getMax() {
return storage.get(0);
}
public int popMax() {
if(storage.size() < 1) return -1;
int max = storage.get(0);
storage.set(0, storage.get(storage.size() - 1));
storage.remove(storage.size() - 1);
maxHeapify(0);
return max;
}
public void increase(int index, int key) {
if(key < storage.get(index)) return;
storage.set(index, key);
int i = index;
while(i > 0 && storage.get(getParent(i)) < storage.get(i)) {
int tmp = storage.get(i);
storage.set(i, storage.get(getParent(i)));
storage.set(storage.get(getParent(i)), tmp);
i = getParent(i);
}
}
public void addKey(int key) {
storage.add(-1);
increase(storage.size() - 1, key);
}
}
}
计数排序
计数排序是我目前最喜欢的排序方法,由它所延伸出来的排序法也各有特色。
它的算法还有有点创意的,居然可以做到不比较就排序。看了下,其实核心代码有两段:
for j ← 1 to length[A]
do C[A[j]] ← C[A[j]] + 1
这段代码还是很酷的,就是说,在A[j]这个值对应的C中的位置,存储了A[j]出现的次数。eg. A [1,2,4,6,2,8,4]
那么C的大小就8,在跑完这个循环之后,得到的结果是: C[1,2,0,2,0,1,0,1]
第二段核心代码:
for i ← 1 to k
do C[i] ← C[i] + C[i - 1]
C中的每个位置i,将保存A表中的值比i小或者等于i的数目。结果是这样: C[1,3,3,5,5,6,6,7]
就是说在原始表A中,大于等于1的有1个,大于等于2的有3个,大于等于4的有5个
最后创建一个新的结果集,对于A中的每个值,根据C[A[j]],也就是大于等于这个值的数目,来决定它在结果集中的位置,然后把C里面的值减一。
for j ← length[A] downto 1
do B[C[A[j]]] ← A[j]
C[A[j]] ← C[A[j]] - 1
时间复杂度是O(n) 空间复杂度是O(n + k),n是输入列表的长度,k是其最大值。所以当输入列表中的值在很大范围内时,该方法就不适用了。
package com.ibm.billyao.sort;
import java.util.ArrayList;
import java.util.List;
public class CoutingSort implements Sortable {
@Override
public void sort(List<Integer> list, int order) {
int val = findLargest(list);
List<Integer> tmpList = new ArrayList<Integer>();
for(int i = 0; i <= val; i++) {
tmpList.add(0);
}
List<Integer> result = coutingSort(tmpList, list);
System.out.println(result);
}
private List<Integer> coutingSort(List<Integer> tmpList, List<Integer> list) {
for(int i = 0 ; i < list.size(); i++) {
int index = list.get(i);
tmpList.set(index, tmpList.get(index) + 1);
}
for(int i = 1; i < tmpList.size(); i++) {
tmpList.set(i, tmpList.get(i) + tmpList.get(i - 1));
}
List<Integer> result = new ArrayList<Integer>();
for(int i = 0; i < list.size(); i++) {
result.add(0);
}
for(int i = list.size() - 1; i >= 0; i--) {
int index = list.get(i);
int val = tmpList.get(index);
result.set(val - 1, index);
tmpList.set(index, tmpList.get(index) - 1);
}
return result;
}
private int findLargest(List<Integer> list) {
int result = Integer.MIN_VALUE;
for(int val : list) {
result = (val > result) ? val : result;
}
return result;
}
}
基数排序
大致看了下基数排序的思想,其实就是对于每个数字的每一位进行排序,从最小位开始排,然后依次向前。
而对于每一位的排序,因为这个时候的取值空间就非常小了,0-9(十进制),那么使用counting sort的时机就比较合适。
想到这里,想起昨天做的一些思考,对于counting sort,我们可以先做一个数值匹配,举个例子input A[1, 1000, 10000, 50000, 8000]
对于这样一个列表,counting sort其实并不是很合适,为什么呢?他的取值空间太大了,从1到50000,空间复杂度很高。
我想,应该可以有这么个办法来解决,首先做一个映射
1 - 1
2 - 1000
3 - 10000
4 - 500000
5 - 8000
然后把原始表中的值替换掉,再来排序,这个时候空间复杂度只有O(n),其实可能都不到。这个时候再来做排序。等排序结束了之后,通过这个映射,再把正确的值映射回去。
可以说是完美的解决了计数排序的缺陷,却有保持了它的优点。
那么,再回头想想,这个改良过的计数排序法,跟我们现在学的基数排序,似乎更有优势, 觉得这种基数排序就完全失去必要了。。。
维基上说的是 : 此算法适合所需排列的元素取值范围不大的情况下,否则会造成空间的消耗,比如,一共100个元素,其取值范围从1-100000,显然这个时候用基数排序是不合适的。
但是换个思路考虑,如果你的输入是一位一位的呢?就像基数排序诞生的地方,打孔机。 这种情况下,基数排序支持边输出边排序,而不用等到全部数据都获得了才开始排,显然是要快很多的。
再举个例子,给你的数据是,年,月,日分开来的,然后让你排序,这种情况下,基数排序还是比较不错的选择。
桶排序
刚看了这个排序方法之后,我就有一种似曾相识的感觉,为什么呢?这个根本就是hash的设计原理嘛。
对于输入的列表,我们做了一个假设,就是它的值是均匀分布的,怎么叫均匀分布呢,就是他的值在取值范围的任意值概率都一样。至于这个取值范围,我们在计数排序中已经学过的映射方法,几乎可以把取值控制在一个很小的范围。
那么对于任意一个值,我们都把它放到对应的结果集里,做法跟计数排序一模一样。
那它和计数排序的区别在哪里呢?计数排序在结果集的每个位置上,仅仅是记录了这个值出现的次数,而桶排序则把每个值(注意,这些值有可能不一样,仅仅是落在了相同的域中)都以链表的形式挂在那个位置上。因为概率分布均匀,每个链表上的数据都不会很多,我们可以用一个简单的算法把每个链表进行排序。
整个过程结束之后,结果集其实就相当于排序完成了。
如果把结果列表的index和某个hashcode值对应起来,那么这个结果集基本上就是个HashMap了。当用户检索数据时,首先根据hashcode值找到一个链表,这个链表中的hashcode值都是接近的,然后再通过equals方法找到相应的值。这里链表搜索,因为我们是排序好了的,所以也可以采取二维搜索来加快速度。
至此,一个HaspMap就被设计完成了。
回想一下,桶排序跟基数排序的基础都在于计数排序,它的设计思想真的是太好了,提供了许多种可能。
刚才再看了一遍写的东西,“如果在把果列表的index和某个hashcode值对应起来”,这个对应应该怎么做呢? 用hash?呵呵,又回到起点了。我觉得倒是可以这样,对于任意一个hashcode,比如说50,取某个数字的模,如17,得到的结果就是50%17=16,那么16就是它的index。
所以说,取哪个数字当模,以及如何设计hashcode,都至关重要的影响着hashmap的速度。
插入排序
比较简单的排序算法,性能较低,只推荐在数值较少时排序使用
时间复杂度: O(n^2)
空间复杂度: O(1)
package com.ibm.billyao.sort;
import java.util.ArrayList;
import java.util.List;
public class InsertionSort implements Sortable {
@Override
public void sort(List<Integer> list, int order) {
if(list == null || list.size() == 0) return;
for(int i = 1; i < list.size(); i++) {
int val = list.get(i);
//sort
int j = i - 1;
if(order == 0) {
while(j >= 0 && val < list.get(j)) {
list.set(j + 1, list.get(j));
j--;
}
} else {
while(j >= 0 && val > list.get(j)) {
list.set(j + 1, list.get(j));
j--;
}
}
list.set(j + 1, val);
}
}
}
最后,作为家庭作业,设计了一个简化版本的HashMap。
时间复杂度 O(1)
空间复杂度 O(n/17)
package com.ibm.billyao.sort;
import java.util.ArrayList;
import java.util.List;
public class NewHashMap <K, V>{
private List<Entity<K, V>>[] storage = null;
private static final int MODE = 17;
public NewHashMap() {
storage = new ArrayList[MODE];
}
public V get(K key) {
int index = key.hashCode() % MODE;
List<Entity<K, V>> list = storage[index];
V v = null;
if(null != list) {
v = search(list, key);
}
return v;
}
public void set(K key, V value) {
int index = key.hashCode() % MODE;
List<Entity<K, V>> list = storage[index];
if(null == list) {
list = new ArrayList<Entity<K, V>>();
storage[index] = list;
}
list.add(new Entity<K, V>(key, value));
}
private V search(List<Entity<K, V>> list, K key) {
for(Entity<K, V> entity : list) {
if(entity.getKey().equals(key)) {
return entity.getValue();
}
}
return null;
}
class Entity<K, V> {
private K key;
private V value;
public Entity(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
public static void main(String[] args) {
NewHashMap<String, Integer> map = new NewHashMap<String, Integer>();
map.set("name", 1);
map.set("value", 123);
map.set("test", 13);
System.out.println("name = " + map.get("name"));
System.out.println("value = " + map.get("value"));
System.out.println("test = " + map.get("test"));
}
}
search方法还可能有其它实现,目前这个方法在大数据量下性能明显不足。
在java中,求hash值的方法相对复杂点
/**
* Applies a supplemental hash function to a given hashCode, which defends
* against poor quality hash functions. This is critical because HashMap
* uses power-of-two length hash tables, that otherwise encounter collisions
* for hashCodes that do not differ in lower bits. Note: Null keys always
* map to hash 0, thus index 0.
*/
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
它的解释稍稍让人困惑,因为hashmap使用了2的倍数长度的table,因此,如果两个hashcode在低位字节上没有区别,就会造成冲突。
这句话做何种解释呢?暂存疑惑。