1. 程式人生 > >JAVA中如何用shuffle打亂列表並生成亂序序列

JAVA中如何用shuffle打亂列表並生成亂序序列

引言

在研究用遺傳演算法等啟發式演算法解決旅行商問題(Traveling Salesman Problem,TSP)時,首先要解決的問題時如何生成一個初始解,即一個代表顧客位置的編碼序列,如有5個顧客,如何生成1,2,3,4,5的亂序序列,一般情況下是這樣生成的:

方法一:

/*
	 * @para len represents the length of the solution array
	 * 
	 * function you input a length of an array,this method will return an array 
	 * of number from 1 to len with unsorted.
	 */
	public int[] createSolution(int len) {
		int solutionArr[] = new int[len];
		Random random = new Random();
		int j = 0;
		solutionArr[0] = random.nextInt(len) + 1;
		for (int i = 1; i < len; i++) {
			j = 0;
			while (j != i) {
				j = 0;
				solutionArr[i] = random.nextInt(len) + 1;
				while (j < i && solutionArr[j] != solutionArr[i])
					j++;
			}
		}
		return solutionArr;
	}
如上述程式碼所示,給定一個引數len,上述方法可以返回一個返回從1到len的一個len大小的亂序陣列,很顯然,上述方式生成一個亂序陣列的方式是非常低效的。其實我們可以先生成一個順序陣列再想辦法將其順序打亂,程式碼如下:

方法二:

/*
	 * @para len represents the length of the solution array
	 * 
	 * function you input a length of an array,this method will return an array
	 * of number from 1 to len with unsorted.
	 */
	public int[] createSolution1(int len) {
		int solutionArr[] = new int[len];
		Random random = new Random();
		for (int i = 0; i < len; i++)
			solutionArr[i] = i + 1;
		int endIndex = len / 2, ranIndex1 = 0, ranIndex2 = 0;
		for (int i = 0; i <= endIndex; i++) {
			ranIndex1 = random.nextInt(len);
			ranIndex2 = random.nextInt(len);
			while (ranIndex1 == ranIndex2)
				ranIndex2 = random.nextInt(len);
			swap(solutionArr, ranIndex1, ranIndex2);
		}
		return solutionArr;
	}

         用上述方式比第一種方式減少了許多運算量,但是整個陣列的亂序效果卻不是很好,有沒有一種效果很好的同時運算量又非常小的亂序方式呢。不用同時生成兩個randomIndex,生成一個就行,然後從頭到尾與randomIndex進行交換即可,程式碼如下:

方法二的改進版:

public int[] createSolution3(int len) {
		int solutionArr[] = new int[len];
		int ranIndex=0;
		Random random = new Random();
		for (int i = 0; i < len; i++)
			solutionArr[i] = i + 1;
		for (int i = 0; i <len; i++) {
			ranIndex = random.nextInt(len);
			swap(solutionArr, ranIndex, i);
		}
		return solutionArr;
	}

方法二的改進版的實現方式為從頭到尾將每個元素與隨機生成的下標所對應的元素進行交換以達到相應的亂序效果。

方法三:

其實java.util.Collections
裡面提供了一個shuffle的介面,它可以很方便地將一個有序陣列進行亂序處理。

/*
	 * @para len represents the length of the solution array
	 * 
	 * function you input a length of an array,this method will return an array
	 * of number from 1 to len with unsorted.
	 */
	public Integer[] createSolution2(int len) {
		Integer solutionArr[] = new Integer[len];
		List list=new ArrayList<Integer>();
		for (int i = 0; i < len; i++)
			list.add(i+1);
		Collections.shuffle(list);
		list.toArray(solutionArr);
		return solutionArr;
	}

從eclipse檢視shuffle介面的實現原始碼:

/**
     * Randomly permute the specified list using the specified source of
     * randomness.  All permutations occur with equal likelihood
     * assuming that the source of randomness is fair.<p>
     *
     * This implementation traverses the list backwards, from the last element
     * up to the second, repeatedly swapping a randomly selected element into
     * the "current position".  Elements are randomly selected from the
     * portion of the list that runs from the first element to the current
     * position, inclusive.<p>
     *
     * This method runs in linear time.  If the specified list does not
     * implement the {@link RandomAccess} interface and is large, this
     * implementation dumps the specified list into an array before shuffling
     * it, and dumps the shuffled array back into the list.  This avoids the
     * quadratic behavior that would result from shuffling a "sequential
     * access" list in place.
     *
     * @param  list the list to be shuffled.
     * @param  rnd the source of randomness to use to shuffle the list.
     * @throws UnsupportedOperationException if the specified list or its
     *         list-iterator does not support the <tt>set</tt> operation.
     */
    public static void shuffle(List<?> list, Random rnd) {
        int size = list.size();
        if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
            for (int i=size; i>1; i--)
                swap(list, i-1, rnd.nextInt(i));
        } else {
            Object arr[] = list.toArray();

            // Shuffle array
            for (int i=size; i>1; i--)
                swap(arr, i-1, rnd.nextInt(i));

            // Dump array back into list
            ListIterator it = list.listIterator();
            for (int i=0; i<arr.length; i++) {
                it.next();
                it.set(arr[i]);
            }
        }
    }
        從上述程式碼可以知道,shuffle的引數為一個List列表和一個Random物件,當List比較大時,選擇首先將list通過list.toArray()轉換成陣列,然後按方法二的改進形式交換陣列中元素的值,最後將list中的值依次替換為陣列中的值,返回list物件。
        其實,方法二的改進方法和方法三種shuffle的在JAVA中的實現方式是類似的,很簡單不是嗎?而這種思路就來自 Ronald Fisher 和 Frank Yates首先提出的Fisher–Yates shuffle洗牌演算法,但該演算法的適合計算機運算的版本是由Richard Durstenfeld和Donald E. Knuth發揚光大的,Durstenfeld將該演算法的時間複雜度由Fisher–Yates 提出演算法的O(n*n)降低到O(n),其演算法的偽碼如下:
-- To shuffle an array a of n elements (indices 0..n-1):
for i from n−1 downto 1 do
     j ← random integer such that 0 ≤ j ≤ i
     exchange a[j] and a[i]
該演算法的另外一個版本為從最小的index開始至最高的index的過程:
-- To shuffle an array a of n elements (indices 0..n-1):
for i from 0 to n−2 do
     j ← random integer such that 0 ≤ j < n-i
     exchange a[i] and a[i+j]
https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle