java中文排序問題

 

Java中,對一個數組或列表(在本文中統稱爲集合)中的元素排序,是一個很常常的事情。好在Sun公司在Java庫中實現了大部分功能。若是集合中的元素實現了Comparable接口,調用如下的靜態(static)方法,就能夠直接對集合排序。html

// 數組排序方法
// 數組中的元素能夠是像int這樣的原生類型(primitive type), 也能夠是像String這樣實現了Comparable接口的類型,這裏用type表示。
java.util.Arrays.sort(type[] a);java

// 列表
public static <T> void sort(List<T> list)c++

以上的這些排序方式能知足大部分應用。但集合中的元素沒有實現Comparable接口,或者集合中的元素要一種特別的方式排序,這要怎麼辦?Sun公司早就想到了,並在Java庫中提供上面兩個方法的重載。程序員

// 數組排序方法。
// 數組中的元素能夠是像int這樣的原生類型(primitive type), 也能夠是像String這樣實現了Comparable接口的類型,這裏用type表示。
public static <T> void sort(T[] a, Comparator<? super T> c)數組

// 列表
public static <T> void sort(List<T> list, Comparator<? super T> c)函數

只要實現了Comparator接口,就能夠程序員本身的意思去排序了。對於包含漢字的字符串來講,排序的方式主要有兩種:一種是拼音,一種是筆畫。漢字是經過必定的編碼方式存儲在計算機上的,主要的編碼有:Unicdoe、GB2312和GBK等。測試

Unicode 編碼中的漢字

Unicode中編碼表分爲兩塊,一個是基本的,一個是輔助的。如今的大多數操做系統還不支持Unicode中輔助區域中的文字,如WinXp。編碼

Java中的字符就是Unicode碼錶示的。對於Unicode基本區域中的文字,用兩個字節的內存存儲,用一個char表示,而輔助區域中的文字用4個字節存儲,所以輔助區域中的就要用兩個char來表示了(表一種藍色底就是輔助區域中的文字)。一個文字的unicode編碼,在Java中統一用codePoint(代碼點)這個概念。spa

中文和日文、韓文同樣是表意文字,在Unicode中,中日韓三國(東亞地區)的文字是統一編碼的。CJK表明的就是中日韓。在這裏,我把這3中文字,都做爲漢字處理了。(日語和韓語可能就是從漢語中衍生的吧!)操作系統

漢字在Unicode中的分佈大體以下表:

  首字編碼 尾字編碼 個數
基本漢字 U4E00 U9FBF 20928
異性字 UF900 UFAFF 512
擴展A U3400 U4D8F 512
擴展B U20000 U2A6DF 42720
補充 U2F800 U2FA1F 544
其餘     ...
表一

在這些編碼區間,有些編碼是保留的。

GB2312編碼

GB2312是中華人民共和國最先的計算機漢字編碼方式。大概有6000多個漢字,這些漢字是拼音順序編碼的。這6000多個漢字都是簡體中文字。

GBK編碼

GB2312的擴展,併兼容GB2312。擴展後的漢字大概有2萬多個,其中有簡體漢字也有繁體漢字。

拼音排序

拼音有好幾種方式,其中最主要的是中華人民共和國的漢語拼音 Chinese Phonetic。對漢字的排序有兩種:一種是寬鬆的,可以拼音排序最經常使用的漢字,另外一種是嚴格的,可以拼音排序絕大部分大部分漢字。

寬鬆的拼音排序法

原理:漢字最先是GB2312編碼,收錄了六千多個漢字,是拼音排序的,編碼是連續的。 後來出現了GBK編碼,對GB2312進行了擴展,到了兩萬多漢字,而且兼容GB2312,也就是說GB2312中的漢字編碼是原封不動搬到GBK中的(在GBK編碼中[B0-D7]區中)。

若是咱們只關心這6000多個漢字的順序,就能夠用下面的方法實現漢字寬鬆排序。

/**
* @author Jeff
*
* Copyright (c) 複製或轉載本文,請保留該註釋。
*/

package chinese.utility;

import java.text.Collator;
import java.util.Comparator;
import java.util.Locale;

public class PinyinSimpleComparator implements Comparator<String> {
    public int compare(String o1, String o2) {
        return Collator.getInstance(Locale.CHINESE).compare(o1, o2);
    }
}

在對[孫, 孟, 宋, 尹, 廖, 張, 徐, 昆, 曹, 曾,怡]這幾個漢字排序,結果是:[曹, 昆, 廖, 孟, 宋, 孫, 徐, 尹, 曾, 張, 怡]。最後一個 有問題,不應排在最後的。

注意:這個程序有兩個不足

因爲gb2312中的漢字編碼是連續的,所以新增長的漢字不可能再 照拼音順序插入到已有的gb2312編碼中,因此新增長的漢字不是 拼音順序排的。 同音字比較的結果不等於0 。

下面的測試代碼能夠證實

/**
* @author Jeff
*
* Copyright (c) 複製或轉載本文,請保留該註釋。
*/

/**
* 很是用字(怡)
*/
@Test
public void testNoneCommon() {
    Assert.assertTrue(comparator.compare("怡", "張") > 0);
}

/**
* 同音字
*/
@Test
public void testSameSound() {
    Assert.assertTrue(comparator.compare("怕", "帕") != 0);
}

嚴格的拼音排序法

爲了解決寬鬆的拼音的兩點不足,能夠經過實現漢語拼音的函數來解決。goolge下看到sf上有個pinyin4j的項目,能夠解決這個問題,pinyin4j的項目地址是:http://pinyin4j.sourceforge.net/

實現代碼:

/**
  * @author Jeff
  *
  * Copyright (c) 複製或轉載本文,請保留該註釋。
  */
package chinese.utility;

import java.util.Comparator;
import net.sourceforge.pinyin4j.PinyinHelper;

public class PinyinComparator implements Comparator<String> {

    public int compare(String o1, String o2) {

        for (int i = 0; i < o1.length() && i < o2.length(); i++) {

            int codePoint1 = o1.charAt(i);
            int codePoint2 = o2.charAt(i);

            if (Character.isSupplementaryCodePoint(codePoint1)
                    || Character.isSupplementaryCodePoint(codePoint2)) {
                i++;
            }

            if (codePoint1 != codePoint2) {
                if (Character.isSupplementaryCodePoint(codePoint1)
                        || Character.isSupplementaryCodePoint(codePoint2)) {
                    return codePoint1 - codePoint2;
                }

                String pinyin1 = pinyin((char) codePoint1);
                String pinyin2 = pinyin((char) codePoint2);

                if (pinyin1 != null && pinyin2 != null) { // 兩個字符都是漢字
                    if (!pinyin1.equals(pinyin2)) {
                        return pinyin1.compareTo(pinyin2);
                    }
                } else {
                    return codePoint1 - codePoint2;
                }
            }
        }
        return o1.length() - o2.length();
    }

    /**
     * 字符的拼音,多音字就獲得第一個拼音。不是漢字,就return null。
     */
    private String pinyin(char c) {
        String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c);
        if (pinyins == null) {
            return null;
        }
        return pinyins[0];
    }
}

測試:

/**
  * @author Jeff
  *
  * Copyright (c) 複製或轉載本文,請保留該註釋。
  */
package chinese.utility.test;

import java.util.Comparator;

import org.junit.Assert;
import org.junit.Test;

import chinese.utility.PinyinComparator;

public class PinyinComparatorTest {

    private Comparator<String> comparator = new PinyinComparator();

    /**
     * 經常使用字
     */
    @Test
    public void testCommon() {
        Assert.assertTrue(comparator.compare("孟", "宋") < 0);
    }

    /**
     * 不一樣長度
     */
    @Test
    public void testDifferentLength() {
        Assert.assertTrue(comparator.compare("他奶奶的", "他奶奶的熊") < 0);
    }

    /**
     * 和非漢字比較
     */
    @Test
    public void testNoneChinese() {
        Assert.assertTrue(comparator.compare("a", "阿") < 0);
        Assert.assertTrue(comparator.compare("1", "阿") < 0);
    }

    /**
     * 很是用字(怡)
     */
    @Test
    public void testNoneCommon() {
        Assert.assertTrue(comparator.compare("怡", "張") < 0);
    }

    /**
     * 同音字
     */
    @Test
    public void testSameSound() {
        Assert.assertTrue(comparator.compare("怕", "帕") == 0);
    }

    /**
     * 多音字(曾)
     */
    @Test
    public void testMultiSound() {
        Assert.assertTrue(comparator.compare("曾經", "曾迪") > 0);
    }

}

個人這樣嚴格的拼音排序仍是有有待改進的地方,看上面測試代碼的最後一個測試,就會發現:程序不會根據語境來判斷多音字的拼音,僅僅是簡單的取多音字的第一個拼音。

筆畫排序

筆畫排序,就要實現筆畫比較器。

class StokeComparator implements Comparator<String>

若是有個方法能夠求得漢字的筆畫數,上面的功能就很容易實現。如何求一個漢字的筆畫數?最容易想到的就是查表法。建一個漢字筆畫數表,如:

漢字 Unicode編碼 筆畫數
U4E00 1
U4E8C 2
U9F8D 16
... ... ...
表二

若是是連續的、unicode編碼排好順序的表,實際存儲在筆畫數表中的只需最後一列就夠了。

那如何建這個表呢?這個表存儲在哪裏?

建漢字筆畫數表

如今大多數系統還只能支持Unicode中的基本漢字那部分漢字,編碼從U9FA6-U9FBF。因此咱們只建這部分漢字的筆畫表。漢字筆畫數表,咱們能夠照下面的方法生成:

用java程序生成一個文本文件(Chinese.csv)。包括全部的從U9FA6-U9FBF的字符的編碼和文字。利用excel的 筆畫排序功能,對Chinese.csv文件中的內容排序。 編寫Java程序分析Chinese.csv文件,求得筆畫數, 生成ChineseStroke.csv。矯正筆畫數,從新 漢字的Unicode編碼對ChineseStroke.csv文件排序。 只保留ChineseStroke.csv文件的最後一列,生成Stroke.csv。

在這裏下載上面3個步驟生成的3個文件

生成Chinese.csv的Java程序

/**
  * @author Jeff
  *
  * Copyright (c) 複製或轉載本文,請保留該註釋。
  */
package chinese.utility.preface;

import java.io.IOException;
import java.io.PrintWriter;

public class ChineseCoder {

    public static void main(String[] args) throws IOException {
        PrintWriter out = new PrintWriter("Chinese.csv");
        // 基本漢字
        for(char c = 0x4E00; c <= 0x9FA5; c++) {
            out.println((int)c + "," + c);
        }
        out.flush();
        out.close();

    }

}

初始化筆畫數

從Excel排序事後的Chinese.csv文件來看,排好序的文件仍是有必定規律的。在文件的第9行-12行能夠看出:逐行掃描的時候,當unicode會變小了,筆畫數也就加1。

20059,乛
20101,亅
19969,丁
19970,丂

用下面的Java程序分析吧。

/**
  * @author Jeff
  *
  * Copyright (c) 複製或轉載本文,請保留該註釋。
  */
package chinese.utility.preface;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;

public class Stroke {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        Scanner in = new Scanner(new File("Chinese.csv"));       
        PrintWriter out = new PrintWriter("ChineseStroke.csv");
        String oldLine = "999999";
        int stroke = 0;
        while (in.hasNextLine()) {
            String line = in.nextLine();
            if (line.compareTo(oldLine) < 0) {
                stroke++;               
            }
            oldLine = line;
            out.println(line + "," + stroke);           
        }
        out.flush();
        out.close();
        in.close();
    }

}

上面用的這個規律有問題嗎?有問題,從ChineseStroke.csv文件抽取最後幾個漢字就發現,筆畫數不對。爲何呢?

筆畫數可能不是連續的。 n+1筆畫數的最小Unicode碼可能比n筆畫數的最大Unicode碼要大

咱們要人工覈對ChineseStroke文件,但只要覈對在筆畫變化的那幾個漢字的筆畫數。最後,我發現,只有筆畫數多於30的少數幾個漢字的筆畫數不對。覈對並矯正筆畫數後,用ExcelUnicode從新排序,去掉漢字和Unicode兩列,只保留筆畫數那列,獲得Stroke.csv文件。

求得筆畫數的方法和筆畫比較器方法求得筆畫數的方法測試代碼:

/**
  * @author Jeff
  *
  * Copyright (c) 複製或轉載本文,請保留該註釋。
  */
package chinese.utility.test;

import static org.junit.Assert.assertEquals;

import org.junit.Before;
import org.junit.Test;
import chinese.utility.Chinese;

public class StrokeTest {

    Chinese chinese;

    @Before
    public void setUp() {
        chinese = new Chinese();
    }

    @Test
    public void testStroke() {
        assertEquals(1, chinese.stroke('一'));
    }

    @Test
    public void testStroke2() {
        assertEquals(2, chinese.stroke('二'));
    }

    @Test
    public void testStroke16() {
        assertEquals(16, chinese.stroke('龍'));
    }

    @Test
    public void testStrokeABC() {
        assertEquals(-1, chinese.stroke('a'));
    }

}

求得筆畫數的方法代碼

/**
  * @author Jeff
  *
  * Copyright (c) 複製或轉載本文,請保留該註釋。
  */
package chinese.utility;

import java.util.Comparator;

public class StrokeComparator implements Comparator<String> {

    public int compare(String o1, String o2) {

        Chinese chinese = new Chinese();

        for (int i = 0; i < o1.length() && i < o2.length(); i++) {
            int codePoint1 = o1.codePointAt(i);
            int codePoint2 = o2.codePointAt(i);
            if (codePoint1 == codePoint2)
                continue;

            int stroke1 = chinese.stroke(codePoint1);
            int stroke2 = chinese.stroke(codePoint2);

            if (stroke1 < 0 || stroke2 < 0) {
                return codePoint1 - codePoint2;
            }

            if (stroke1 != stroke2) {
                return stroke1 - stroke2;
            }
        }

        return o1.length() - o2.length();
    }
}

筆畫比較器測試

/**
  * @author Jeff
  *
  * Copyright (c) 複製或轉載本文,請保留該註釋。
  */
package chinese.utility.test;

import java.util.Comparator;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import chinese.utility.StrokeComparator;

public class StrokeComparatorTest {

    private Comparator<String> comparator;
    @Before
    public void setUp() {
        comparator = new StrokeComparator();
    }

    /**
     * 相同筆畫數
     */
    @Test
    public void testCompareEquals() {
        Assert.assertTrue(comparator.compare("一", "丨") == 0);
    }
    /**
     * 不一樣筆畫數
     */
    @Test
    public void testCompare() {
        Assert.assertTrue(comparator.compare("一", "二") < 0);
        Assert.assertTrue(comparator.compare("唔", "馬") > 0);
    }
    /**
     * 長度不一樣
     */
    @Test
    public void testCompareDefficultLength() {
        Assert.assertTrue(comparator.compare("二", "二一") < 0);
    }
    /**
     * 非漢字的比較
     */
    @Test
    public void testABC() {
        Assert.assertTrue(comparator.compare("一", "a") > 0);
        Assert.assertTrue(comparator.compare("a", "b") < 0);       
    }
}

筆畫比較器

/**
  * @author Jeff
  *
  * Copyright (c) 複製或轉載本文,請保留該註釋。
  */
package chinese.utility.test;

import java.util.Comparator;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import chinese.utility.StrokeComparator;

public class StrokeComparatorTest {

    private Comparator<String> comparator;
    @Before
    public void setUp() {
        comparator = new StrokeComparator();
    }

    /**      * 相同筆畫數      */     @Test     public void testCompareEquals() {         Assert.assertTrue(comparator.compare("一", "丨") == 0);     }     /**      * 不一樣筆畫數      */     @Test     public void testCompare() {         Assert.assertTrue(comparator.compare("一", "二") < 0);         Assert.assertTrue(comparator.compare("唔", "馬") > 0);     }     /**      * 長度不一樣      */     @Test     public void testCompareDefficultLength() {         Assert.assertTrue(comparator.compare("二", "二一") < 0);     }     /**      * 非漢字的比較      */     @Test     public void testABC() {         Assert.assertTrue(comparator.compare("一", "a") > 0);         Assert.assertTrue(comparator.compare("a", "b") < 0);            } }