一:字符串的排序
1:小整数键——键索引计数法
- 计算键出现的频率
- 将频率转换为在排序结果中的起始索引位置
- 在辅助数组中开始排序
public class KeyIndexSort {
//字符串
private Alphabet[] alphabets;
//记录键的频率
private int[] count;
//临时数组
private Alphabet[] temp;
public KeyIndexSort(Alphabet[] alphabets,int digit,int indexLen){
count = new int[indexLen + 1];
temp = new Alphabet[alphabets.length];
this.alphabets = alphabets;
count(digit);
change(digit);
category(digit);
}
//统计频率
public void count(int digit){
for (int i = 0; i < alphabets.length; i++) {
count[alphabets[i].toChar(digit) + 1]++;
}
}
//转换起始索引
public void change(int digit){
for (int i = 0; i < count.length - 1; i++) {
count[i+1] += count[i];
}
}
//重新分类
public void category(int digit){
for (int i = 0; i < alphabets.length; i++) {
int index = alphabets[i].toChar(digit);
int tempIndex = count[index];//此时为起始索引
count[index]++;
temp[tempIndex] = alphabets[i];//把原来字符串的值按起始索引重新组合
}
for (int i = 0; i < temp.length; i++) {
alphabets[i] = temp[i];
}
}
public Alphabet[] getAlphabets() {
return alphabets;
}
}
2:等长字符串——低位优先排序
从字符串的右边向左边,以每个位置作为键,用键索引法排序N次
/**
* 低位优先
*/
public class LittleEndianSort {
private KeyIndexSort keyIndexSort;
private Alphabet[] alphabets;
public LittleEndianSort(Alphabet[] alphabets){
this.alphabets = alphabets;
}
public void sort(){
int indexLen = alphabets[0].getIndexLen();
int len = alphabets[0].R();
//从低位数开始排序
for (int i = len - 1; i >= 0 ; i--) {
keyIndexSort = new KeyIndexSort(alphabets,i,indexLen);
alphabets = keyIndexSort.getAlphabets();
}
}
public void show(){
for (int i = 0; i < alphabets.length; i++) {
System.out.printf("%s ",alphabets[i]);
}
}
}
3:变长字符串——高位优先排序
从左向右遍历字符串,把字符串分为多个开头相同的子串,将已被检察过的子串排在前面,通过递归将其他未检查的串排序
- 在时间上:在比较小字符串时可以通过插入排序比较
- 在空间上:如果相同前缀的字符串过多,将会耗费大量空间
/**
* 高位优先
*/
public class HighEndianSort {
private KeyIndexSort keyIndexSort;
private Alphabet[] alphabets;
public HighEndianSort(Alphabet[] alphabets){
this.alphabets = alphabets;
}
public void sort(){
int indexLen = alphabets[0].getIndexLen();
if (indexLen >= Character.MAX_VALUE)
indexLen = Byte.MAX_VALUE;
//开始排序
keyIndexSort = new KeyIndexSort(alphabets,0,indexLen,0,alphabets.length - 1);
}
public void show(){
for (int i = 0; i < alphabets.length; i++) {
System.out.printf("%s ",alphabets[i]);
}
}
public class KeyIndexSort {
//字符串
private Alphabet[] alphabets;
//记录键的频率
private int[] count;
//临时数组
private Alphabet[] temp;
//低位排序
public KeyIndexSort(Alphabet[] alphabets,int digit,int indexLen){
count = new int[indexLen + 1];
temp = new Alphabet[alphabets.length];
this.alphabets = alphabets;
count(digit);
change();
category(digit);
}
//高位排序
public KeyIndexSort(Alphabet[] alphabets,int digit,int indexLen,int low,int high){
this.alphabets = alphabets;
if (high <= low)
return;
// if (high - low <= 15)
// InSertSort.insert_sort(alphabets);
count = new int[indexLen + 2];//空出一位处理变长的字符串
temp = new Alphabet[alphabets.length];
count(digit,low,high);
change();
category(digit,low,high);
//从左到右排序,在以首位字符开头的字符串数组内排序
for (int i = 0; i < indexLen; i++) {
this.alphabets = new KeyIndexSort(this.alphabets,digit+1,indexLen,low+count[i],low+count[i+1]-1).getAlphabets();
}
}
//统计频率
public void count(int digit){
for (int i = 0; i < alphabets.length; i++) {
count[alphabets[i].toChar(digit) + 1]++;
}
}
public void count(int digit,int low,int high){
for (int i = low; i <= high; i++) {
if (alphabets[i].toChar(digit) == Character.MAX_VALUE)
count[1] ++; //表示长度为digit的字符串已经结束,记录长度为digit的字符串的数量
else
count[alphabets[i].toChar(digit) + 2]++;
}
}
//转换起始索引
public void change(){
for (int i = 0; i < count.length - 1; i++) {
count[i+1] += count[i];
}
}
//重新分类
public void category(int digit){
for (int i = 0; i < alphabets.length; i++) {
int index = alphabets[i].toChar(digit);
int tempIndex = count[index];//此时为起始索引
count[index]++;
temp[tempIndex] = alphabets[i];//把原来字符串的值按起始索引重新组合
}
for (int i = 0; i < temp.length; i++) {
alphabets[i] = temp[i];
}
}
//重新分类,分类后count记录为结束索引+1
public void category(int digit,int low,int high){
for (int i = low; i <= high; i++) {
int index = alphabets[i].toChar(digit);
int tempIndex;
if (index == Character.MAX_VALUE) {
tempIndex = count[0];
count[0]++; //长度为digit的字符串个数
}
else {
tempIndex = count[index + 1];
count[index+1]++;
}
temp[tempIndex] = alphabets[i];//把原来字符串的值按起始索引重新组合
}
for (int i = low; i <= high; i++) {
alphabets[i] = temp[i];
}
}
public Alphabet[] getAlphabets() {
return alphabets;
}
}
//字符串插入排序
public static Alphabet[] insert_sort(Alphabet[] arr, int low, int high, int digit){
for (int i = low + 1; i <= high; i++) {
if(arr[i].toString().substring(digit).compareTo(arr[i-1].toString().substring(digit)) < 0) //比前面的数小
{
Alphabet temp = arr[i];
int j;
for (j = i; j > low && temp.toString().substring(digit).compareTo(arr[j-1].toString().substring(digit)) < 0; j--) //找到合适的位置
arr[j] = arr[j-1];
arr[j] = temp; //把数字插进去
}
}
return arr;
}
4:改善的快速排序:三向快速排序
从高位开始,使用待比较的字符作为切分。使得左边的字符串比他小,右边的字符串比他大,中间的字符串都是以该字符开头的,可以直接跳过同一个字符再排序。
特点: 可以更好的处理等值键,公共前缀键,小数组
/**
* 三向快速排序
*/
public class Quick3Sort {
private Alphabet[] alphabets;
public Quick3Sort(Alphabet[] arr){
alphabets = arr;
quick_sort(alphabets,0,arr.length - 1,0);
}
//快速排序
public static Alphabet[] quick_sort(Alphabet[] arr, int low, int high, int digit){
// if(high <= low + 10) //对于小数组,使用插入排序
// {
// InSertSort.insert_sort(arr,low,high,digit);
// return arr;
// }
if (high <= low)
return arr;
int key; //记录当前的切分字符
if (arr[low].toChar(digit) == Character.MAX_VALUE)
key = -1;
else
key = arr[low].toChar(digit);
int[] pivot = find_pivot(arr,low,high,key,digit); //获取基准值的下标,便于分割数组
quick_sort(arr,low,pivot[0]-1,digit); //左边排序
if (key >= 0)
quick_sort(arr,pivot[0],pivot[1],digit+1); //中间排序,忽略首字符
quick_sort(arr,pivot[1]+1,high,digit); //右边排序
return arr;
}
//找基准值的下标
public static int[] find_pivot(Alphabet[] arr,int low,int high,int key,int digit){
int i = low+1;
while (i <= high)
{
//记录当前下标的字符
int temp = arr[i].toChar(digit);
if (temp == Character.MAX_VALUE)
temp = -1;
//当前字符和切分的字符进行比较,保证左边比较小,右边比较大
if (temp < key)
SortUtil.swap(low++,i++,arr);
else if (temp > key)
SortUtil.swap(i,high--,arr);
else
i++;
}
int[] pivot = {low,high};
return pivot;
}
public void show(){
for (int i = 0; i < alphabets.length; i++) {
System.out.printf("%s ",alphabets[i]);
}
}
}
二:字符串的查找
1:单词查找树
根据单词的索引对应的字符,在指定的子树内逐层查找
- 适用于字母表和键较小的情况,需要耗费空间
//获取键值
public T get(String key){
StringTreeNode<T> node = get(key,root,0);
if (node == null)
return null;
return node.getValue();
}
private StringTreeNode<T> get(String key,StringTreeNode<T> node,int digit){
//在串的范围内查找
if (node == null)
return null;
if (digit == key.length())
return node;
int index = alphabet.toIndex(key.charAt(digit));
return get(key,node.getNodes()[index],digit+1);//在对应的子树查找
}
}
2:三向查找树
根据单词的索引对应的字符,分别在左子树,中子树,右子树内查找
遍历时对子树的顺序有要求,同时每个结点自身带有一个键
//获取键值
public T get(String key){
ThreeStringTreeNode<T> node = get(key,root,0);
if (node == null)
return null;
return node.getValue();
}
public ThreeStringTreeNode<T> get(String key,ThreeStringTreeNode<T> node,int digit){
if (key.length() == 0)
return root;
if (node == null)
return null;
int index = alphabet.toIndex(key.charAt(digit));
//对三个子节点进行比较
if (alphabet.toChar(index) < node.getKey())
return get(key, node.getLeft(), digit);
else if (alphabet.toChar(index) > node.getKey())
return get(key, node.getRight(), digit);
//如果是中间结点,看看是不是够长度了
else if (digit < key.length() - 1)
return get(key, node.getMid(), digit+1);
else
return node;
}
三:字符串的查找
1:暴力查找
逐个对照文本和模式串
public int search(String pattern){
int index = -1;
int pLen = pattern.length();
//i指向文本的开头
for (int i = 0; i < len; i++) {
index = i;
//j指向模式串的开头
for (int j = 0; j < pLen; j++) {
if (text.charAt(i + j) != pattern.charAt(j))
{
index = -1;
break;
}
}
if (index != -1)
return index;
}
return index;
}
public int searchClear(String pattern){
int index = -1;
int pLen = pattern.length();
int i,j;
//i指向文本的末尾,j指向模式串的开头
for (i = 0,j = 0; i < len && j <pLen; i++) {
if (text.charAt(i) != pattern.charAt(j)){
i -= j;
j = 0;
}
else
j++;
}
if (j == pLen)
return i - pLen;
return index;
}
2:KMP算法
提前判断在文本中重新查找的定位,不会重新回退文本指针,而是回退模式指针。
DFA:有限状态转换机——dfa[文本字符][模式字符]
定义:
模式中的每个字符对应一个状态,每个状态可以转换为任意字符。其中只有一种转换 “从左指向右” 是匹配的,其他转换 “指向左侧” 是不匹配的。
转换过程:
- 从文本前进,检查字符( i+1)
- 对于文本的字符,检查转换机。
- 如果模式的字符匹配,则转换机向右移动(j+1)
- 如果模式的字符不匹配,则转换机向左移动
(j = dfa[text.charAt(i)][pattern.charAt(j)])
- 如果转换机到达停止状态,则说明匹配了模式字符串
- 如果在文本结束后还未到达停止状态,则没有匹配
构造:扫描模式字符串
忽略首位:需右移
扫描 第1位到第j-1位
忽略最后一位:已经匹配失败了
例子:ABABC
-
状态0:A-[开始状态] ——> 开始右移
-
状态1:A B-[0] [0] ——> 没有相同的,重新开始匹配
-
状态2:A B-[0] A-[0] [1] ——>与状态0相同,从状态1继续匹配
-
状态3:A B-[0] A-[0] B-[1] [2] ——>与状态1相同,从状态2继续匹配
-
状态5:A B-[0] A-[0] B-[1] C-[停止状态]——>匹配结束
//创建状态机
public void createDFA(String pattern){
dfa = new int[alphabet.R()][pattern.length()];
//忽略模式串的首位,状态机启动
dfa[pattern.charAt(0)][0] = 1;
for (int lastIndex = 0,nextIndex = 1; nextIndex < pattern.length(); nextIndex++) {
//若匹配失败,状态机返回上一状态
for (int alphabetIndex = 0; alphabetIndex < alphabet.R(); alphabetIndex++) {
dfa[alphabetIndex][nextIndex] = dfa[alphabetIndex][lastIndex];
}
//匹配成功,状态机向前继续走 2,3,4....
dfa[pattern.charAt(nextIndex)][nextIndex] = nextIndex + 1;
//记录状态机的上一状态
lastIndex = dfa[pattern.charAt(nextIndex)][lastIndex];
}
}
查找
public int search(String pattern){
createDFA(pattern);
int index = -1;
int pLen = pattern.length();
int i,j;
//i指向文本的末尾,j指向模式串的开头
for (i = 0,j = 0; i < len && j <pLen; i++) {
j = dfa[text.charAt(i)][j];
}
if (j == pLen)
index = i - pLen;
return index;
}
2:Boyer-Moore算法
从右向左扫描模式字符串,在文本中回退
匹配失败:
- 字符不被包含:应该检查文本的下一位,将模式字符串与已知模式字符串对齐。否则字符会重叠
- 字符被包含:使用right数组(记录字符最右位置),检查文本的第x位,将模式字符串与字符出现的最右位置对齐。否则该字符会与更右边的字符重叠
- 保证字符向右移动
public void initialize(String pattern){
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
right[c] = i;
}
}
public int search(String pattern){
initialize(pattern);
int index = -1;
int pLen = pattern.length();
int skip;
//i指向文本的开头,j指向模式串的末尾
for (int i = 0; i <= len - pLen; i += skip) {
skip = 0;
for (int j = pLen - 1; j >= 0; j--) {
char c = text.charAt(i + j);
if (c != pattern.charAt(j)){
//对齐模式字符串,模式串必须向前移动,避免重叠
skip = j - right[c];
if (skip <= 0)
skip = 1;
break;
}
}
//匹配
if (skip == 0)
return i;
}
return index;
}
3:Rabin-Karp算法
长度为M的字符串,对应于R进制的M位数。需要用一张大小为Q的散列表保存键
- 计算模式字符串的散列值
- 计算文本中与模式字符串相同长度的子字符串的散列值,如果散列值和模式字符串相同再验证是否匹配。
基本方法:
- 除留取余法:适用于5位以内的数值
- Hornor:适用于多位数值,但是成本高
for (int i = 0; i < target.length(); i++) {
hash = (R * hash + target.charAt(i)) % prime;
}
- 高效Hornor:[i~M-1]的字符串的值=它减去第一个数,乘R,加上最后一个数。
textHash = hash(text,pLen);
textHash = (textHash + Q - RM * text.charAt(i - pLen) % Q) % Q;
textHash = (textHash * R + text.charAt(i)) % Q;
查找
private boolean check(int i) {
return true;
}
private boolean check(int i,String pattern) {
for (int j = 0; j < pattern.length(); j++)
if (pattern.charAt(j) != text.charAt(i + j))
return false;
return true;
}
//获得第M-1个R 余 Q
private void initialRM(String pattern){
int pLen = pattern.length();
this.RM = 1;
for (int i = 0; i < pLen; i++) {
RM = (R * RM) % Q;
}
}
//hornor哈希
private long hash(String key, int M) {
long h = 0;
for (int j = 0; j < M; j++)
h = (R * h + key.charAt(j)) % Q;
return h;
}
public int search(String pattern){
initialRM(pattern);
int pLen = pattern.length();
this.textHash = hash(text,pLen);
long pHash = hash(pattern,pLen);
//一开始就命中
if (pHash == textHash && check(0))
return 0;
for (int i = pLen; i < len; i++) {
//减去第一个数
textHash = (textHash + Q - RM * text.charAt(i - pLen) % Q) % Q;
//加上最后一个数
textHash = (textHash * R + text.charAt(i)) % Q;
if (pHash == textHash)
if (check(i - pLen + 1))
return i - pLen + 1;
}
return -1;
}
四:字符串的压缩
系统的输入输出都是基于8位的字节流,也就是基于二进制的比特流。能够通过压缩将比特流最小化。
1:X位编码
对于字母表的种类和大小固定的字符串,可以直接针对字母表进行编码。把压缩个数从原来八位编码减少到N位编码。
- 2种字母(正负对错):单位编码(0,1)
- 4种字母(基因碱基):双位编码(00,01,10,11)
- 8种字母(八仙过海):三位编码(000,001,010,011,100,101,110,111)
- 2N种字母:log2N位编码
(1)压缩
- 确定字符串的字符数量,方便解码顺利进行
- 根据字符在字母表的索引和编码位数,压缩为01编码
(2)解压缩
- 根据编码位数,读取字符的索引
- 根据字母表和字符索引,解压缩对应字符
2:游程编码
游程是指连续的0或1的字符串。对于短游程相对较少,长游程较多的长比特流(位图),可以直接针对游程编码,将连续的0或1直接表示为它的个数,并且使用4位或8位对个数编码、输出。
(1)压缩
- 从0开始,读取0或1,并记录个数
- 若个数超过范围,则压缩个数,再压缩0,方便继续读取该字符
- 若字符不同,则压缩个数
(2)解压缩
从0开始,根据个数,不断输出0或1
3:霍夫曼编码
对于一段普通的字符串,将出现频率高的字符用较少的位数编码,出现频率较低的字符用较多的位数编码,同时字符编码都不是其他字符编码的前缀码。
本质:两个链接的单词查找树
把0和1看为单词查找树中的键,把字符看为单词查找树对应的值,则构造单词查找树相当于对字符进行霍夫曼编码:每个字符的编码就是从根节点到该叶子节点的路径。
(1)构造二叉树
- 读取字符串,获取N个包含字符及其出现的频率的树,加入优先队列
//统计频率
public void initFrequence(){
for (int i = 0; i < input.length; i++)
frequences[input[i]]++;
}
- 不断取两个最小的树,生成一个新的父节点和一棵新的树,树的根节点的频率为他们的和。
- 只剩下一个结点时,为单词查找树的根节点。
//创建树
public void createTree(){
//初始化多个树
for (int i = 0; i < input.length; i++) {
int freq = frequences[input[i]];
if (freq == 0 ) //忽略无关的字符
continue;
HuffmanNode node = new HuffmanNode(input[i],freq);
pq.insert(node);
}
//开始创建
while (pq.getSize() > 1){
HuffmanNode left = pq.delMaximum();
HuffmanNode right = pq.delMaximum();
HuffmanNode parent = new HuffmanNode('\0',left.getFrequence()+right.getFrequence(),left,right);
pq.insert(parent);
}
root = pq.delMaximum();
}
(2)构建映射表
从根节点开始记录路径,向左移动的同时字符+0,向右移动的同时字符+1,遇到叶子节点则获得目标字符及其路径编码,保存在映射表中
//建立映射
private void codeTree(HuffmanNode node,String s){
if (node.isLeaf())
{
int index = alphabet.toIndex(node.getValue());
codes[index] = s;
return;
}
codeTree(node.getLeft(),s+'0');
codeTree(node.getRight(),s+'1');
}
(3)传输树
- 对树进行前序遍历,遇到内部结点输出0,遇到叶子结点输出1并且将其字符的ASCALL八码编码输出。
//输出树
private void transferTree(HuffmanNode node){
if (node.isLeaf())
{
treeCode += "1";
int index = alphabet.toIndex(node.getValue());
treeCode += codes[index];
return;
}
treeCode += "0";
transferTree(node.getLeft());
transferTree(node.getRight());
}
- 遍历字符串,遇到1则构造叶子节点并加入字符,遇到0则构造父节点并递归构造左右子树。
//解码树
private HuffmanNode decodeTree() {
if (index == input.length)
return null;
if (input[index] == '1') {
return new HuffmanNode(input[index], 0, null, null);
} else {
index++;
HuffmanNode left = decodeTree();
index++;
HuffmanNode right = decodeTree();
return new HuffmanNode('\0', 0, left, right);
}
}
(3)压缩
- 构造编码单词查找树
- 根据树构造映射表
- 根据映射表编码
//根据明文,映射表压缩
public String compress(){
StringBuffer output = new StringBuffer();
for (int i = 0; i < input.length; i++) {
int index = alphabet.toIndex(input[i]);
String code = codes[index];
for (int j = 0; j < code.length(); j++) {
if (code.charAt(j) == '1')
output.append('1');
else
output.append('0');
}
}
return output.toString();
}
(4)解压缩
- 读取一棵树
- 使用该树解码:从根节点开始,遇到0向左移动,遇到1向右移动,遇到叶子节点则获得字符。
//根据密文解压缩
public String expand() {
StringBuffer output = new StringBuffer();
HuffmanNode node = root;
for (int i = 0; i < input.length;) {
while (!node.isLeaf()){
if (input[i++] == '0')
node = node.getLeft();
else
node = node.getRight();
}
output.append(node.getValue());
node =root;
}
return output.toString();
}
4:LZW编码
维护一张字符串键和定长编码的编译表,用十六进制表示8位编码。
- 00-7F:符号表中的128个单字符的键(最前补0)
- 80:保留80,作为文本结束的标志
- 81-FF:将其他的编码值分配给子字符串
(1)压缩
- 找出输入的字符串在符号表中的最长前缀,并输出前缀的编码。
- 继续查找下一字符,将前缀的编码和该字符的组合作为字母表的新建,扩展编码递增。
public static String compress(){
StringBuffer result = new StringBuffer();//编码结果
int code = EOF; //用80标志文件结束
while (input.length() > 0){
//获取最长前缀,输出编码
String prefix = tree.longestPrefix(input);
result.append(tree.get(prefix));
//如果文本还未结束并且在字母表范围中,继续查找下一字符组合,扩展编码递增
if (prefix.length() < input.length() && code < sum)
tree.put(input.substring(0,prefix.length()+1),code++);
input = input.substring(prefix.length());
}
result.append(R); //输出结束标记
return result.toString();
}
(2)解压缩
- 根据编码X,在符号表中找到和X匹配的字符串S1,再读取下一个编码获取S2
- 将符号表的下一个扩展值递增,键设为S1+S2的首字母
//构造逆表
public static String[] initST(){
String[] st = new String[sum];
//初始化字母表对应的映射
for (int i = 0; i < R; i++)
st[i] = ""+(char)i;
st[EOF] = " "; //结束标记
return st;
}
public static String expand(){
StringBuffer result = new StringBuffer();//编码结果
String[] st = initST();
int found = EOF+1; //记录扩展编码
//从密文第一个编码开始,根据逆表获取字符
int index = alphabet.toChar(input.charAt(0));
String value = st[index];
for (int i = 1; i < input.length(); i+=width) {
//输出编码对应字符串
result.append(value);
//获取下一个编码
index = alphabet.toChar(input.charAt(i));
//超出字母表范围
if (index == R)
break;
String temp = st[index];
//如果前缀码不可用,则根据上一个字符串的首字母获取新的前缀码
if (index == found)
temp = value + value.charAt(0);
//在编码范围之内,则扩展编码,添加新的键值对
if (found < sum)
st[found++] = value + temp.charAt(0);
value = temp;
}
return result.toString();
}