`
mabusyao
  • 浏览: 247910 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

排序算法总结

阅读更多
花了一个月时间学习了算法导论上的排序算法,学而不思则惘,现在把学习中的经验心得,都记录下来。

排序
        -- 交换排序
                -- 冒泡排序 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在低位字节上没有区别,就会造成冲突。

这句话做何种解释呢?暂存疑惑。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics