算法(第四版)C# 习题题解——3.1

时间:2021-02-17 15:32:39

写在前面

新增了一行内容。

整个项目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csharp

查找更方便的版本见:https://alg4.ikesnowy.com/

这一节内容可能会用到的库文件有 SymbolTable,同样在 Github 上可以找到。

善用 Ctrl + F 查找题目。

习题&题解

3.1.1

解答

官方解答:https://algs4.cs.princeton.edu/31elementary/GPA.java.html

ST.java:https://algs4.cs.princeton.edu/code/edu/princeton/cs/algs4/ST.java.html

建立一个符号表,然后把键值放进去,读取计算即可。

和上一章节用过的方法类似,先定义了一个接口 IST<Key, Value> ,包含书中提到的基本 API。

然后定义类 ST ,用标准库里面的 Dictionary 实现了 IST

代码
using System.Collections;
using System.Collections.Generic; namespace SymbolTable
{
/// <summary> 利用库函数实现的标准符号表。 </summary>
public class ST<Key, Value> : IST<Key, Value>, IEnumerable<Key>
{
private Dictionary<Key, Value> st; /// <summary> 新建一个符号表。 </summary>
public ST() => this.st = new Dictionary<Key, Value>(); /// <summary> 检查符号表中是否存在与键 <paramref name="key"/> 对应的值。 </summary>
public virtual bool Contains(Key key) => this.st.ContainsKey(key); /// <summary> 从符号表中删除键 <paramref name="key"/> 及对应的值。 </summary>
public virtual void Delete(Key key) => this.st.Remove(key); /// <summary> 获取键 <paramref name="key"/> 对应的值,不存在时返回 null。 </summary>
public virtual Value Get(Key key) => this.st[key]; /// <summary> 获取枚举器。 </summary>
public IEnumerator<Key> GetEnumerator() => this.st.Keys.GetEnumerator(); /// <summary> 检查符号表是否为空。 </summary>
public virtual bool IsEmpty() => this.st.Count == 0; /// <summary> 获得符号表中所有键的集合。 </summary>
public virtual IEnumerable<Key> Keys() => this.st.Keys; /// <summary> 向符号表中插入新的键值对。 </summary>
public virtual void Put(Key key, Value value) => this.st.Add(key, value); /// <summary> 获取符号表中键值对的数量。 </summary>
public virtual int Size() => this.st.Count; /// <summary> 获取枚举器。 </summary>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
另请参阅

SymbolTable 库

3.1.2

解答

官方解答:https://algs4.cs.princeton.edu/31elementary/ArrayST.java.html

建立两个数组,分别存放键和值,一一对应。

添加时直接将新键值对放到数组最后即可。

删除时将待删除的键值对和位于最后的键值对交换,然后将其置空即可。

代码
using System;
using System.Collections.Generic; namespace SymbolTable
{
/// <summary>
/// 符号表(数组实现)。
/// </summary>
/// <typeparam name="Key">键类型。</typeparam>
/// <typeparam name="Value">值类型。</typeparam>
public class ArrayST<Key, Value> : IST<Key, Value>
{
private Key[] keys; // 键数组
private Value[] values; // 值数组
private int n = 0; // 键值对数目 /// <summary>
/// 建立基于数组实现的符号表。
/// </summary>
public ArrayST() : this(8) { } /// <summary>
/// 建立基于数组实现的符号表。
/// </summary>
/// <param name="initCapacity">初始大小。</param>
public ArrayST(int initCapacity)
{
this.keys = new Key[initCapacity];
this.values = new Value[initCapacity];
} /// <summary>
/// 检查键 <typeparamref name="Key"/> 是否存在。
/// </summary>
/// <param name="key">需要检查是否存在的键。</param>
/// <returns></returns>
public bool Contains(Key key) => Get(key).Equals(default(Key)); /// <summary>
/// 删除键 <paramref name="key"/> 及对应的值。
/// </summary>
/// <param name="key">需要删除的键。</param>
public void Delete(Key key)
{
for (int i = 0; i < this.n; i++)
{
if (key.Equals(this.keys[i]))
{
this.keys[i] = this.keys[this.n - 1];
this.values[i] = this.values[this.n - 1];
this.keys[this.n - 1] = default(Key);
this.values[this.n - 1] = default(Value);
this.n--;
if (this.n > 0 && this.n == this.keys.Length / 4)
Resize(this.keys.Length / 2);
return;
}
}
} /// <summary>
/// 获取键对应的值,若键不存在则返回 null。
/// </summary>
/// <param name="key">需要查找的键。</param>
/// <returns></returns>
public Value Get(Key key)
{
for (int i = 0; i < this.n; i++)
if (this.keys[i].Equals(key))
return this.values[i];
return default(Value);
} /// <summary>
/// 检查符号表是否为空。
/// </summary>
/// <returns></returns>
public bool IsEmpty() => this.n == 0; /// <summary>
/// 获得包含全部键的集合。
/// </summary>
/// <returns></returns>
public IEnumerable<Key> Keys()
{
Key[] result = new Key[this.n];
Array.Copy(this.keys, result, this.n);
return result;
} /// <summary>
/// 向符号表中插入新元素,若键存在将被替换。
/// </summary>
/// <param name="key">键。</param>
/// <param name="value">值。</param>
public void Put(Key key, Value value)
{
Delete(key); if (this.n >= this.values.Length)
Resize(this.n * 2); this.keys[this.n] = key;
this.values[this.n] = value;
this.n++;
} /// <summary>
/// 返回符号表中键值对的数量。
/// </summary>
/// <returns>键值对数量。</returns>
public int Size() => this.n; /// <summary>
/// 为符号表重新分配空间。
/// </summary>
/// <param name="capacity">新分配的空间大小。</param>
private void Resize(int capacity)
{
Key[] tempKey = new Key[capacity];
Value[] tempValue = new Value[capacity]; for (int i = 0; i < this.n; i++)
tempKey[i] = this.keys[i];
for (int i = 0; i < this.n; i++)
tempValue[i] = this.values[i]; this.keys = tempKey;
this.values = tempValue;
}
}
}
另请参阅

SymbolTable 库

3.1.3

解答

基于无序链表的官方实现:https://algs4.cs.princeton.edu/31elementary/SequentialSearchST.java.html

有序符号表的 API 见书中表 3.1.4(中文版 P230,英文版 P366)。

在官方实现的基础上修改 Put 方法,先找到合适位置再插入新的键值对,保证链表有序。

为方便插入操作,可以使用双向链表作为基础进行实现。

表中同时维护开头和末尾引用,加快获得最值的速度。

代码
using System;
using System.Collections.Generic; namespace SymbolTable
{
/// <summary>
/// 基于有序链表的有序符号表实现。
/// </summary>
/// <typeparam name="Key">符号表键类型。</typeparam>
/// <typeparam name="Value">符号表值类型。</typeparam>
public class OrderedSequentialSearchST<Key, Value> : IST<Key, Value>, IOrderedST<Key, Value>
where Key : IComparable<Key>
{
/// <summary>
/// 符号表结点。
/// </summary>
private class Node
{
public Key Key { get; set; } // 键。
public Value Value { get; set; } // 值。
public Node Next { get; set; } // 后继。
public Node Prev { get; set; } // 前驱。
} private Node first = null; // 起始结点。
private Node tail = null; // 末尾结点。
private int n = 0; // 键值对数量。 /// <summary>
/// 构造基于有序链表的有序符号表。
/// </summary>
public OrderedSequentialSearchST() { } /// <summary>
/// 大于等于 key 的最小值。
/// </summary>
/// <returns></returns>
public Key Ceiling(Key key)
{
Node pointer = this.tail;
while (pointer != null && Greater(key, pointer.Key))
pointer = pointer.Prev;
return pointer == null ? default(Key) : pointer.Key;
} /// <summary>
/// 键 <paramref name="key"/> 在表中是否存在对应的值。
/// </summary>
/// <param name="key">键。</param>
/// <returns></returns>
public bool Contains(Key key) => Floor(key).Equals(key); /// <summary>
/// 从表中删去键 <paramref name="key"/> 对应的值。
/// </summary>
/// <param name="key">键。</param>
public void Delete(Key key)
{
Node pointer = this.first;
while (pointer != null && !pointer.Key.Equals(key))
pointer = pointer.Next;
if (pointer == null)
return;
Delete(pointer);
} /// <summary>
/// 从链表中删除结点 <paramref name="node"/>。
/// </summary>
/// <param name="node">待删除的结点。</param>
private void Delete(Node node)
{
Node prev = node.Prev;
Node next = node.Next;
if (prev == null)
this.first = next;
else
prev.Next = next; if (next == null)
this.tail = prev;
this.n--;
} /// <summary>
/// 删除最大的键。
/// </summary>
public void DeleteMax()
{
if (this.n == 0)
throw new Exception("ST Underflow");
Delete(this.tail);
} /// <summary>
/// 删除最小的键。
/// </summary>
public void DeleteMin()
{
if (this.n == 0)
throw new Exception("ST Underflow");
Delete(this.first);
} /// <summary>
/// 小于等于 Key 的最大值。
/// </summary>
/// <returns></returns>
public Key Floor(Key key)
{
Node pointer = this.first;
while (pointer != null && Less(key, pointer.Key))
pointer = pointer.Next;
return pointer == null ? default(Key) : pointer.Key;
} /// <summary>
/// 获取键 <paramref name="key"/> 对应的值,不存在则返回 null。
/// </summary>
/// <param name="key">键。</param>
/// <returns></returns>
public Value Get(Key key)
{
Node pointer = this.first;
while (pointer != null && Greater(key, pointer.Key))
pointer = pointer.Next; if (pointer == null)
return default(Value);
else if (pointer.Key.Equals(key))
return pointer.Value;
else
return default(Value);
} /// <summary>
/// 符号表是否为空。
/// </summary>
/// <returns></returns>
public bool IsEmpty() => this.n == 0; /// <summary>
/// 获得符号表中所有键的集合。
/// </summary>
/// <returns></returns>
public IEnumerable<Key> Keys() => this.n == 0 ? new List<Key>() : Keys(this.first.Key, this.tail.Key); /// <summary>
/// 获得符号表中 [<paramref name="lo"/>, <paramref name="hi"/>] 之间的键。
/// </summary>
/// <param name="lo">范围起点。</param>
/// <param name="hi">范围终点。</param>
/// <returns></returns>
public IEnumerable<Key> Keys(Key lo, Key hi)
{
List<Key> list = new List<Key>();
Node pointer = this.first;
while (pointer != null && Less(pointer.Key, lo))
pointer = pointer.Next;
while (pointer != null && Less(pointer.Key, hi))
{
list.Add(pointer.Key);
pointer = pointer.Next;
}
if (pointer.Key.Equals(hi))
list.Add(pointer.Key);
return list;
} /// <summary>
/// 最大的键。
/// </summary>
/// <returns></returns>
public Key Max() => this.tail == null ? default(Key) : this.tail.Key; /// <summary>
/// 最小的键。
/// </summary>
/// <returns></returns>
public Key Min() => this.first == null ? default(Key) : this.first.Key; /// <summary>
/// 向符号表插入键值对,重复值将被替换。
/// </summary>
/// <param name="key">键。</param>
/// <param name="value">值。</param>
public void Put(Key key, Value value)
{
Delete(key); Node temp = new Node()
{
Key = key,
Value = value,
Prev = null,
Next = null
}; Node left = null, right = this.first;
while (right != null && Less(right.Key, temp.Key))
{
left = right;
right = right.Next;
} Insert(left, right, temp); if (left == null)
this.first = temp;
if (right == null)
this.tail = temp;
this.n++;
} /// <summary>
/// 小于 Key 的键的数量。
/// </summary>
/// <returns></returns>
public int Rank(Key key)
{
int counter = 0;
Node pointer = this.first;
while (pointer != null && Less(pointer.Key, key))
{
pointer = pointer.Next;
counter++;
}
return counter;
} /// <summary>
/// 获得排名为 k 的键(从 0 开始)。
/// </summary>
/// <param name="k">排名</param>
/// <returns></returns>
public Key Select(int k)
{
if (k >= this.n)
throw new Exception("k must less than ST size!"); Node pointer = this.first;
for (int i = 0; i < k; i++)
pointer = pointer.Next;
return pointer.Key;
} /// <summary>
/// 获得符号表中键值对的数量。
/// </summary>
/// <returns></returns>
public int Size() => this.n; /// <summary>
/// [<paramref name="lo"/>, <paramref name="hi"/>] 之间键的数量。
/// </summary>
/// <param name="lo">范围起点。</param>
/// <param name="hi">范围终点。</param>
/// <returns></returns>
public int Size(Key lo, Key hi)
{
int counter = 0;
Node pointer = this.first;
while (pointer != null && Less(pointer.Key, lo))
pointer = pointer.Next;
while (pointer != null && Less(pointer.Key, hi))
{
pointer = pointer.Next;
counter++;
}
return counter;
} /// <summary>
/// 键 <paramref name="a"/> 是否小于 <paramref name="b"/>。
/// </summary>
/// <param name="a">检查是否较小的键。</param>
/// <param name="b">检查是否较大的键。</param>
/// <returns></returns>
private bool Less(Key a, Key b) => a.CompareTo(b) < 0; /// <summary>
/// 键 <paramref name="a"/> 是否大于 <paramref name="b"/>。
/// </summary>
/// <param name="a">检查是否较大的键。</param>
/// <param name="b">检查是否较小的键。</param>
/// <returns></returns>
private bool Greater(Key a, Key b) => a.CompareTo(b) > 0; /// <summary>
/// 将结点 <paramref name="k"/> 插入到 <paramref name="left"/> 和 <paramref name="right"/> 之间。
/// </summary>
/// <param name="left">作为前驱的结点。</param>
/// <param name="right">作为后继的结点。</param>
/// <param name="insert">待插入的结点。</param>
private void Insert(Node left, Node right, Node k)
{
k.Prev = left;
k.Next = right;
if (left != null)
left.Next = k; if (right != null)
right.Prev = k;
}
}
}
另请参阅

SymbolTable 库

3.1.4

解答

利用 Time 类型记录时间,用 Event 来记录事件内容。

Time 类型包含时分秒三个 int 变量,同时实现 IComparable 接口。

Event 类型只包含事件的名称,相当于对 string 做了一个封装。

随后以 Time 为键类型,Event 为值类型,利用上一题编写的有序符号表进行操作。

代码

Time 类

using System;
using System.Text; namespace _3._1._4
{
/// <summary>
/// 时间类。
/// </summary>
public class Time : IComparable<Time>
{
public int Hour { get; set; }
public int Minute { get; set; }
public int Second { get; set; } public Time() : this(0, 0, 0) { } public Time(int hour, int minute, int second)
{
this.Hour = hour;
this.Minute = minute;
this.Second = second;
} public int CompareTo(Time other)
{
int result = this.Hour.CompareTo(other.Hour);
if (result == 0)
result = this.Minute.CompareTo(other.Minute);
if (result == 0)
result = this.Second.CompareTo(other.Second);
return result;
} public override bool Equals(object obj)
{
if (this == obj)
return true;
return CompareTo((Time)obj) == 0;
} public override int GetHashCode()
{
int result = 1;
result += this.Hour;
result *= 31;
result += this.Minute;
result *= 31;
result += this.Second;
return result;
} public override string ToString()
{
StringBuilder sb = new StringBuilder();
sb.Append(this.Hour < 10 ? "0" + this.Hour : this.Hour.ToString());
sb.Append(":");
sb.Append(this.Minute < 10 ? "0" + this.Minute : this.Minute.ToString());
sb.Append(":");
sb.Append(this.Second < 10 ? "0" + this.Second : this.Second.ToString());
return sb.ToString();
}
}
}

Event 类

namespace _3._1._4
{
public class Event
{
public string EventMessage { get; set; } public Event() : this(null) { } public Event(string message)
{
this.EventMessage = message;
} public override string ToString()
{
return this.EventMessage;
}
}
}
另请参阅

SymbolTable 库

3.1.5

解答

官方解答:https://algs4.cs.princeton.edu/31elementary/SequentialSearchST.java.html

size() 方法只需要直接返回当前的 n 值即可。

delete() 方法需要遍历链表,找到对应结点并删除。

keys() 方法只需要根据当前的 n 新建一个数组,把链表中的键值存入即可。

代码
/// <summary>
/// 从表中删去键 <paramref name="key"/> 及其对应的值。
/// </summary>
/// <param name="key">要删除的键。</param>
public void Delete(Key key)
{
if (key == null)
throw new ArgumentNullException("key can't be null");
Node before = null, target = this.first;
while (target != null && !target.Key.Equals(key))
{
before = target;
target = target.Next;
}
if (target != null)
Delete(before, target);
} /// <summary>
/// 从链表中删除指定的结点。
/// </summary>
/// <param name="before"><paramref name="target"/> 的前驱。</param>
/// <param name="target">准备删除的结点。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="target"/> 为 <c>null</c> 时抛出此异常。</exception>
private void Delete(Node before, Node target)
{
if (target == null)
throw new ArgumentNullException("target can't be null"); if (before == null)
this.first = target.Next;
else
before.Next = target.Next;
this.n--;
} /// <summary>
/// 获得所有的键。
/// </summary>
/// <returns>包含所有键的集合。</returns>
public IEnumerable<Key> Keys()
{
Key[] keys = new Key[this.n];
Node pointer = this.first;
for (int i = 0; i < this.n; i++)
{
keys[i] = pointer.Key;
pointer = pointer.Next;
}
return keys;
} /// <summary>
/// 获取符号表中的键值对数量。
/// </summary>
/// <returns>当前符号表中的键值对数量。</returns>
public int Size() => this.n;
另请参阅

SymbolTable 库

3.1.6

解答

FrequencyCounter 的官方实现:https://algs4.cs.princeton.edu/31elementary/FrequencyCounter.java.html

每个单词都会被放进符号表一次,

因此 Put 的调用次数就等于单词总数 W +1(注意寻找最大值的时候有一次 Put 调用)

对于重复的单词,输入时会先调用 Get 获得当前计数之后再 Put 回去。

寻找最大值时,对于符号表中的每个键值都会调用两次 Get。

重复的单词数量 = (W - D)。

因此 Get 方法的调用次数 = (W - D) + 2D

3.1.7

解答

FrequencyCounter 中添加一个 CountDistinct 方法,计算不重复的键数。

public static int CountDistinct<TKey>(TKey[] keys, IST<TKey, int> st)
{
int distinct = 0;
for (int i = 0; i < keys.Length; i++)
{
if (!st.Contains(keys[i]))
st.Put(keys[i], distinct++);
}
return distinct;
}

结果如下:

算法(第四版)C# 习题题解——3.1

另请参阅

SymbolTable 库

3.1.8

解答

FrequencyCounter 的官方实现:https://algs4.cs.princeton.edu/31elementary/FrequencyCounter.java.html

《双城记》:https://introcs.cs.princeton.edu/java/data/tale.txt

官网给出的数据末尾有完整的版权说明,因此使用频率最高的单词变成了版权方的名字 Gutenberg-tm。

去掉末尾的版权声明之后,获得的单词是:Monseigneur

另请参阅

SymbolTable 库

3.1.9

解答

FrequencyCounter 的官方实现:https://algs4.cs.princeton.edu/31elementary/FrequencyCounter.java.html

《双城记》:https://introcs.cs.princeton.edu/java/data/tale.txt

对 FrequencyCounter 做修改,在调用 Put 方法之前,将单词记录在字符串变量 lastPut 中。

在读入单词结束之后输出 lastPutwords 变量。

将末尾的版权信息删除后,得到的结果如下:

算法(第四版)C# 习题题解——3.1

代码
public static string MostFrequentlyWord(string filename, int minLength, IST<string, int> st)
{
int distinct = 0, words = 0;
StreamReader sr = new StreamReader(File.OpenRead(filename)); string[] inputs =
sr
.ReadToEnd()
.Split(new char[] { ' ', '\r', '\n' },
StringSplitOptions.RemoveEmptyEntries); string lastPut = "";
foreach (string s in inputs)
{
if (s.Length < minLength)
continue;
words++;
if (st.Contains(s))
{
lastPut = s;
st.Put(s, st.Get(s) + 1);
}
else
{
lastPut = s;
st.Put(s, 1);
distinct++;
}
} Console.WriteLine("Last Put: " + lastPut + "\t words count: " + words); string max = "";
st.Put(max, 0);
foreach (string s in st.Keys())
if (st.Get(s) > st.Get(max))
max = s; return max;
}
另请参阅

SymbolTable 库

3.1.10

解答

如图所示:

算法(第四版)C# 习题题解——3.1

插入新的键值对需要遍历整个链表,比较次数等于链表在插入前的键值对数目。

修改已有的键值对则需要遍历链表直到找到该键值对,比较次数等于该键值对以及它之前所有键值对的数目。

共比较 0 + 1 + 2 + 3 + 4 + 5 + 6 + 4 + 6 + 7 + 8 + 9 = 55 次。

3.1.11

解答

键的轨迹如下图所示:

算法(第四版)C# 习题题解——3.1

键查找使用二分查找优化,插入新的键时不必与每个键都进行比较。

共进行了 0 + 1 + 2 + 2 + 2 + 3 + 3 + 3 + 3 + 3 + 3 + 4 = 29 次比较。

3.1.12

解答

建立类 Item

public class Item<TKey, TValue> : IComparable<Item<TKey, TValue>>
where TKey : IComparable<TKey>
{
public TKey Key { get; set; }
public TValue Value { get; set; } public int CompareTo(Item<TKey, TValue> other)
{
return this.Key.CompareTo(other.Key);
}
}

之后修改 BinarySearchST,将其中的 TKey[] keysTValue[] values 数组用 Item[] items 数组代替。

例如 keys[i] 变为 items[i].Keyvalues[i] 变为 items[i].Value

添加一个构造函数,调用之前编写的归并排序实现。

/// <summary>
/// 根据已有的键值对构造一个符号表。
/// </summary>
/// <param name="items">已有的键值对。</param>
public ItemBinarySearchST(Item<TKey, TValue>[] items)
{
this.items = new Item<TKey, TValue>[items.Length];
Array.Copy(items, this.items, items.Length);
this.n = items.Length;
MergeSort merge = new MergeSort();
merge.Sort(this.items);
}
另请参阅

Merge 库

SymbolTable 库

3.1.13

解答

Get() 调用次数比 Put() 调用次数多了三个数量级,

BinarySearchSTSequentialSearchST 的平均 Put() 开销是一样的,

因此选择平均 Get() 开销更小的 BinarySearchST

3.1.14

解答

根据上题给出的结论,选择 BinarySearchST

由于 BinarySearchSTSequentialSearchST 执行 Put() 的开销相同

因此选择 Get() 开销更低的 BinarySearchST

3.1.15

解答

假设先全部 Put(),再进行查找操作。

即分别进行 $ 1 $, $ 10 ^ 3 $, $ 10 ^ 6 $ 次插入

$ N = 1 $ 时,可以直接得出比例 $ 0.1 % \(。
\) N = 10 ^ 3 $ 时,

插入耗时 $ = 1 + 2 + ... + 10 ^ 3 = 500500 $,

查询耗时 $ = 10 ^ 6 * \lg(10 ^ 3) = 9965784 $,

比例为 $ 4.782 % \(。
\) N = 10 ^ 6 $ 时

插入耗时 $ = 1 + 2 + ... + 10 ^ 6 = 500000500000 $,

查询耗时 $ = 10 ^ 9 * \lg(10 ^ 6) = 19931568569 $,

比例为 $ 96.17 % ​$。

3.1.16

解答

官方实现:https://algs4.cs.princeton.edu/31elementary/BinarySearchST.java.html

先通过二分查找获得下标,然后后面的元素依次向前移动一位。

public void Delete(TKey key)
{
if (key == null)
throw new ArgumentNullException("argument to Delete() is null");
if (IsEmpty())
return; int i = Rank(key); if (i == this.n && this.keys[i].CompareTo(key) != 0)
return; for (int j = i; j < this.n - 1; j++)
{
this.keys[j] = this.keys[j + 1];
this.values[j] = this.values[j + 1];
} this.n--;
this.keys[this.n] = default(TKey);
this.values[this.n] = default(TValue); if (this.n > 0 && this.n == this.keys.Length / 4)
Resize(this.n / 2);
}
另请参阅

SymbolTable 库

3.1.17

解答

官方实现:https://algs4.cs.princeton.edu/31elementary/BinarySearchST.java.html

先通过二分查找大于等于 key 的键下标 i

如果 keys[i]key 相等则直接返回 keys[i]

否则返回 keys[i-1]

public TKey Floor(TKey key)
{
if (key == null)
throw new ArgumentNullException("argument to Floor() is null");
int i = Rank(key);
if (i < this.n && this.keys[i].CompareTo(key) == 0)
return this.keys[i];
if (i == 0)
return default(TKey);
else
return this.keys[i - 1];
}
另请参阅

SymbolTable 库

3.1.18

解答

设 key 为目标键。

算法初始时 lo = 0, hi = n - 1,数组已排序。

当找到目标键时,返回的下标 mid 显然是正确的。

(0...a[mid - 1] 都小于 a[mid],同时 a[mid] = key)

接下来证明:当目标键不存在时,lo 可以代表小于 key 的键的个数。

由算法内容,当循环退出时,一定有 lo 和 hi 交叉,即 lo > hi。

考虑最后一次循环,必然执行了 lo = mid + 1 或者 hi = mid - 1。

即最后一次循环之后 lo = mid + 1 > hi 或 hi = mid - 1 < lo。

又由于 mid = (lo + hi) / 2,代入有:

即(lo + hi) / 2 + 1 > hi 或(lo + hi) / 2 - 1 < lo

(lo - hi) / 2 + 1 > 0 或(hi - lo) / 2 - 1 < 0

(hi - lo) / 2 < 1

hi - lo < 2

由于 hi 和 lo 都是整数,故有 hi -lo <= 1

由算法的内容可以得出,最后一次循环时,

下标小于 lo 的元素都小于 key,下标大于 hi 的元素都大于 key

且下标小于 lo 的元素正好有 lo 个 (0...lo - 1)。

当 lo = hi 时,mid = lo

若 key > lo,则 lo = lo + 1,即 a[lo] 本身也小于 key。

若 key < lo,lo 不变,即 a[lo] 就是大于 key 的第一个元素。

当 lo = hi - 1 时,mid = lo

若 key > lo,则 lo = lo + 1 = hi,变为上一种情况。

若 key < lo,则 hi = lo - 1,a[lo] 是大于 key 的第一个元素。

综上,Rank() 是正确的。

3.1.19

解答

将频率和当前最大频率相同的单词都放到一个队列里即可。

string max = "";
Queue<string> queue = new Queue<string>();
st.Put(max, 0);
foreach (string s in st.Keys())
{
if (st.Get(s) > st.Get(max))
{
max = s;
queue.Clear();
queue.Enqueue(s);
}
else if (st.Get(s) == st.Get(max))
{
queue.Enqueue(s);
}
}
另请参阅

SymbolTable 库

3.1.20

解答

国内的书中关于命题 B 的证明有错误,新版的证明如下:

算法(第四版)C# 习题题解——3.1

虽然新版还是有个小错误,$ n-2 $ 应该改为 $ k-2 $。

勘误见:https://algs4.cs.princeton.edu/errata/errata-printing11.php

先证单调性,利用数学归纳法:

已知对于 $ N=0 ​$,满足 $ C(0) \le C(1) ​$。

假设对于 $ N=n ​$,满足 $ C(n) \le C(n+1) ​$。

根据递归式,有:

\[\begin{eqnarray*}
& C(n) & \le C(\lfloor n/2 \rfloor) + 1 \\
\\
& C(n+1) & \le
\begin{cases}
C(\lfloor n/2 \rfloor) +1 & \text{$ n $ 是偶数} \\
C(\lfloor n/2 \rfloor + 1) + 1 & \text{$ n $ 是奇数}
\end{cases}\\
\\
& C(n+2) & \le C(\lfloor n/2 \rfloor + 1) + 1
\end{eqnarray*}
\]

又 $ C(n) \le C(n+1) ​$ ,推出 $ C(\lfloor n/2 \rfloor) + 1 \le C(\lfloor n/2 \rfloor + 1) + 1 ​$。

故 $ C(n+1) \le C(n+2) ​\(,由数学归纳法,\) C(n) \le C(n+1) ​$ 成立。

已知当 $ N = 2^k - 1 $ 时,有 $ C(N) \le k = \lg(N+1) \le \lg N + 1$。

接下来证明在 $ (2^k - 1, 2^{k + 1} -1) $ 范围内上式仍然成立。

不妨设 $ 0 < M < 2^k $ ,则有 $ 2^k - 1 < N + M < 2^{k + 1} -1 \(。
转变为证:\) C(N+M) \le \lg (N+M) + 1 $ 。

由于 $ C(N+M) $ 是一个整数,则 $ \lfloor \lg(N+M) +1\rfloor = k+1 $。

即求证: $ C(N+M) \le k+1 $。

由单调性可得 $ C(N+M) \le C(2^{k+1} - 1) \le k+1 ​$,得证。

3.1.21

解答

BinarySearchST

包含一个键数组和一个值数组,以及一个 int 变量。

数组长度变化范围为 N~4N ,故总大小:

从 2 × (24 + 8N) +4 = 52 + 16N 字节 (100 %),

到 2 × (24 + 32N) +4 = 52 + 64N 字节(25 %)之间变动。

SequentialSearchST

包含 N 个结点以及一个 int 变量

(16 + 8 + 8 + 8)N + 4 = 4 + 40N 字节

3.1.22

解答

Get() 做修改,得到 MoveToFrontArrayST

public TValue Get(TKey key)
{
int i;
for (i = 0; i < this.n; i++)
if (this.keys[i].Equals(key))
break; if (i == this.n)
return default(TValue); TKey toFrontKey = this.keys[i];
TValue toFrontValue = this.values[i]; for (int j = i; j > 0; j--)
this.keys[j] = this.keys[j - 1];
for (int j = i; j > 0; j--)
this.values[j] = this.values[j - 1]; this.keys[0] = toFrontKey;
this.values[0] = toFrontValue; return this.values[0];
}
另请参阅

SymbolTable 库

3.1.23

解答

这里的右移操作可以理解为 「小数点前移一位」

即数字依次向右退一位,个位上的数字被舍弃。

对于十进制,小数点前移一位会使 $ n $ 变为 $ \lfloor n / 10 \rfloor $。

同样对于二进制就会使 $ n $ 变为 $ \lfloor n / 2 \rfloor $。

当需要除以 $ 2 $ 的 $ k $ 次幂的时候,可以用右移 $ k $ 位代替并减少时间开销。

同理可以用左移 $ k $ 位来代替乘以 $ 2 $ 的 $ k $ 次幂。

注:

这样会降低程序可读性,

并且某些语言(C / C++)的编译器已经可以自动执行这项优化了。

请充分考虑你的代码受众之后再决定是否采用这种写法。

二分查找的最大查找次数 = $ \lg N + 1$ (见 3.1.20 的证明)

一个数最多被左移的次数也正好等于 $ \lfloor \lg N \rfloor + 1 $

(任意正整数都能被表示为 $ 2 ^ k + m $ 的形式,即 $ k +1 $ 位二进制数)

因此一次二分查找所需的最大比较次数正好是 $ N $ 的二进制表示的位数。

3.1.24

解答

FrequencyCounter 的官方实现:https://algs4.cs.princeton.edu/31elementary/FrequencyCounter.java.html

二分查找总是与中间值进行比较,现在改为与数组中第 x% 位置上的元素比较。

具体而言,$ \frac{k_x-k_{lo}}{k_{hi}-k_{lo}} $ 代表数组在均匀情况下目标值 $ k_x $ 的相对位置(一个比率,在数组第 x% 的位置上)。

那么相对应的下标就等于 $ lo+\frac{k_x-k_{lo}}{k_{hi}-k_{lo}} \times (hi - lo) $。

用这个式子代替原来的 $ mid=lo + (hi-lo)/2 $ 即可。

不难看出这种方法对于分布相对均匀的数组比较有利,相对于二分查找而言迭代次数会少很多。

但如果数组分布不够均匀,也可能表现出不如二分查找的性能。

实验结果也证实了这一判断,就随机数组而言,插值查找相对于二分查找只有 1% 左右的性能提升。

算法(第四版)C# 习题题解——3.1

代码

SearchCompare 在书中没有出现,但可以简单的实现为调用 FrequencyCounter 并计时的方法:

public static long Time<TKey>(IST<TKey, int> st, TKey[] keys)
{
Stopwatch sw = new Stopwatch();
sw.Start();
FrequencyCounter.MostFrequentlyKey(st, keys);
sw.Stop();
return sw.ElapsedMilliseconds;
}

由于这里需要使用数字而非字符串作为键值,需要对官方给出的 FrequencyCounter 做一些修改:

public static TKey MostFrequentlyKey<TKey> (IST<TKey, int> st, TKey[] keys)
{
foreach (TKey s in keys)
{
if (st.Contains(s))
st.Put(s, st.Get(s) + 1);
else
st.Put(s, 1);
} TKey max = keys[0];
foreach (TKey s in st.Keys())
if (st.Get(s) > st.Get(max))
max = s; return max;
}
另请参阅

SymbolTable 库

3.1.25

解答

英文原文指的是 most recently accessed key,因此指的是最近访问的键。

实现比较简单,先在类中定义一个新的成员 cache 作为缓存,

然后修改 Get 方法,在实际查找之前先检查缓存,如果缓存未命中则在查找之后更新它。

要注意的是缓存指向内容的有效性,在数组中指的是下标是否有效,在链表中指的是结点是否为空。

利用《双城记》测试的结果:

算法(第四版)C# 习题题解——3.1

代码

BinarySearchST

cache 是一个 int 类型的变量,代表下标。

在二分查找前先检查缓存,要注意cache超出数组大小的情况。

如果缓存未命中,则进行二分查找,并在返回结果前更新缓存。

public TValue Get(TKey key)
{
if (key == null)
throw new ArgumentNullException("argument to Get() is null");
if (IsEmpty())
return default(TValue);
if (this.cache < this.n && this.keys[this.cache].Equals(key)) // 缓存检查
return this.values[this.cache];
int rank = Rank(key);
if (rank < this.n && this.keys[rank].Equals(key))
{
this.cache = rank; // 更新缓存
return this.values[rank];
} return default(TValue);
}

SequentialSearchST

cache 是一个结点类型的变量,代表一个键值对。

类似的,在顺序查找前先检查缓存,如果缓存未命中则更新缓存。

要注意的是如果缓存的结点被删除,需要将缓存置为 null

Get() 方法

public TValue Get(TKey key)
{
if (key == null)
throw new ArgumentNullException("key can't be null"); if (this.cache != null && this.cache.Key.Equals(key)) // 检查缓存
return this.cache.Value;
for (Node pointer = this.first; pointer != null; pointer = pointer.Next)
{
if (pointer.Key.Equals(key))
{
this.cache = pointer; // 更新缓存
return pointer.Value;
}
}
return default(TValue);
}

Delete() 方法

public void Delete(TKey key)
{
if (key == null)
throw new ArgumentNullException("key can't be null");
Node before = null, target = this.first;
while (target != null && !target.Key.Equals(key))
{
before = target;
target = target.Next;
}
if (target == this.cache) // 删除缓存
this.cache = null;
if (target != null)
Delete(before, target);
}
另请参阅

SymbolTable 库

3.1.26

解答

字典文件:https://introcs.cs.princeton.edu/java/data/web2.txt

《双城记》:https://introcs.cs.princeton.edu/java/data/tale.txt

浏览器可能会直接打开 txt,此时右键链接-目标另存为即可下载。

FrequencyCounter 的官方实现:https://algs4.cs.princeton.edu/31elementary/FrequencyCounter.java.html

我们利用 BinarySearchST 会自动对键排序的性质来实现字典序排序。

首先将字典存到一个符号表中,按照 “单词-序号” 的形式保存。

然后读入文件,如果读入的单词存在于字典中,

则将其以 “序号-单词” 的形式存到 BinarySearchST 中去。

读入完毕后,遍历 BinarySearchST 即可获得字典序的单词列表。

对于按频率排序,我们基于已有的实现修改。

在每次取得最大值之后,输出并删除最大值,如此循环即可获得频率排序的单词序列。

也可以将单词-频率序列全部读出来存放到数组之中,然后用第二章的排序算法排序。

测试结果,取 minLength = 13,只截取了部分。

算法(第四版)C# 习题题解——3.1

算法(第四版)C# 习题题解——3.1

代码
public static void LookUpDictionary(string filename, string dictionaryFile, int minLength)
{
// 初始化字典
StreamReader sr = new StreamReader(File.OpenRead(dictionaryFile));
string[] words = sr.ReadToEnd().Split(new char[] { ' ', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
BinarySearchST<string, int> dictionary = new BinarySearchST<string, int>();
for (int i = 0; i < words.Length; i++)
{
if (words[i].Length > minLength)
dictionary.Put(words[i], i);
}
sr.Close(); // 读入单词
StreamReader srFile = new StreamReader(File.OpenRead(filename));
string[] inputs = srFile.ReadToEnd().Split(new char[] { ' ', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
srFile.Close(); BinarySearchST<int, string> stDictionary = new BinarySearchST<int, string>();
BinarySearchST<string, int> stFrequency = new BinarySearchST<string, int>();
foreach (string s in inputs)
{
if (stFrequency.Contains(s))
stFrequency.Put(s, stFrequency.Get(s) + 1);
else if (dictionary.Contains(s))
{
stFrequency.Put(s, 1);
stDictionary.Put(dictionary.Get(s), s);
}
} // 输出字典序
Console.WriteLine("Alphabet");
foreach (int i in stDictionary.Keys())
{
string s = stDictionary.Get(i);
Console.WriteLine(s + "\t" + stFrequency.Get(s));
} // 频率序
Console.WriteLine("Frequency");
int n = stFrequency.Size();
for (int i = 0; i < n; i++)
{
string max = "";
stFrequency.Put(max, 0);
foreach (string s in stFrequency.Keys())
if (stFrequency.Get(s) > stFrequency.Get(max))
max = s;
Console.WriteLine(max + "\t" + stFrequency.Get(max));
stFrequency.Delete(max);
}
}
另请参阅

SymbolTable 库

3.1.27

解答

事实上就是说,先构造一个包含 N 个不重复键的符号表,然后进行 S 次查找。

给出 S 的增长数量级,使得构造符号表的成本和查找的成本相同。

这里假设一次数组交换和一次比较的成本是相同的。

先考虑构造符号表的成本,一次 Put() 需要调用一次 Rank() 和一次插入操作。

2.1 节插入排序的命题 B 给出了每次插入平均需要移动一半的数组元素的结论。

于是构造符号表所需的成本约为:$n\lg n + \frac{1}{2}\sum_{k=1}^{n} k=n\lg n + \frac{n(n-1)}{4} $ 。

这里查找的成本是这么计算的:$ \lg0+\lg1+\cdots+\lg n < n\lg n $

查找所需的成本比较简单,一次二分查找的比较次数约为 $ \lg n $,总成本就是 $ S\lg n $ 。

令两边相等,解方程即可得到 $ S=n+\frac{n(n-1)}{4\lg n} $ 。

如果用大 O 记法,也可以记为 $ O(n^2 / \lg n) $,如果要选择一个比较常用的上界则可以选择 $ O(n^2) $。

实验结果,两边的成本是很接近的:

算法(第四版)C# 习题题解——3.1

另请参阅

SymbolTable 库

3.1.28

解答

将重新分配数组空间的代码提前,然后添加判断语句即可。

BinarySearchSTOrderedInsertion

/* 省略 */

if (this.n == this.keys.Length)
Resize(this.n * 2); // 如果插入的键比所有键都大则直接插入末尾。
if (this.n == 0 || this.keys[this.n - 1].CompareTo(key) < 0)
{
this.keys[this.n] = key;
this.values[this.n] = value;
this.n++;
return;
} int i = Rank(key); /* 省略 */
另请参阅

SymbolTable 库

3.1.29

解答

官方实现:https://algs4.cs.princeton.edu/31elementary/TestBinarySearchST.java.html

官方实现中有几处错误,需要做一些修改。

/* 省略 */

Console.WriteLine("Testing Select()");
Console.WriteLine("-----------------------------------");
for (int i = 0; i < st.Size(); i++) // 循环条件不能有 '='
Console.WriteLine(i + " " + st.Select(i));
Console.WriteLine(); /* 省略 */ while (!st.IsEmpty())
st.Delete(st.Select(st.Size() / 2));
Console.WriteLine("After deleting the remaining keys");
Console.WriteLine("-----------------------------------");
// 异常处理
try
{
foreach (string s in st.Keys())
Console.WriteLine(s + " " + st.Get(s));
}
catch (Exception ex)
{
Console.WriteLine("Exception: " + ex.Message);
}
Console.WriteLine(); /* 省略 */

结果如下:

size = 10
min = A
max = X Testing Keys()
-----------------------------------
A 8
C 4
E 12
H 5
L 11
M 9
P 10
R 3
S 0
X 7 Testing Select()
-----------------------------------
0 A
1 C
2 E
3 H
4 L
5 M
6 P
7 R
8 S
9 X key Rank Floor Ceil
-----------------------------------
A 0 A A
B 1 A C
C 1 C C
D 2 C E
E 2 E E
F 3 E H
G 3 E H
H 3 H H
I 4 H L
J 4 H L
K 4 H L
L 4 L L
M 5 M M
N 6 M P
O 6 M P
P 6 P P
Q 7 P R
R 7 R R
S 8 S S
T 9 S X
U 9 S X
V 9 S X
W 9 S X
X 9 X X
Y 10 X
Z 10 X After deleting the smallest 3 keys
-----------------------------------
H 5
L 11
M 9
P 10
R 3
S 0
X 7 After deleting the remaining keys
-----------------------------------
Exception: called Min() with empty table After adding back N keys
-----------------------------------
A 8
C 4
E 12
H 5
L 11
M 9
P 10
R 3
S 0
X 7
另请参阅

SymbolTable 库

3.1.30

解答

官方实现:https://algs4.cs.princeton.edu/31elementary/BinarySearchST.java.html

首先在 BinarySearchST 中添加如下方法。

/// <summary> 检查符号表结构是否有效。 </summary>
private bool Check() => IsSorted() && RankCheck(); /// <summary> 检查 <see cref="keys"/> 数组是否有序。 </summary>
private bool IsSorted()
{
for (int i = 1; i < Size(); i++)
if (this.keys[i].CompareTo(this.keys[i - 1]) < 0)
return false;
return true;
} /// <summary> 检查 Rank(Select(i)) = i 是否成立。 </summary>
private bool RankCheck()
{
for (int i = 0; i < Size(); i++)
if (i != Rank(Select(i)))
return false;
for (int i = 0; i < Size(); i++)
if (keys[i].CompareTo(Select(Rank(this.keys[i]))) != 0)
return false;
return true;
}

然后在 Put()Delete() 方法的末尾添加:Debug.Assert(Check()); 即可。

另请参阅

SymbolTable 库

3.1.31

解答

性能测试方法构造如下:

先编写一个随机字符串方法,生成一个长度大于 50 的字符串(作为未命中访问)。

然后随机生成符合要求的字符串数组,将它们全部放入符号表中。

然后遍历 10 次生成的字符串数组,对于数组中的每个元素都进行一次命中查询。

同时在每次命中查询的同时都进行一次未命中查询即可。

测试结果:

算法(第四版)C# 习题题解——3.1

代码

按照要求编写代码,在 SearchCompare 类里添加一个 Random random 成员,并添加如下方法:

随机字符串发生器:

public static string GetRandomString(int minLength, int maxLength)
{
int length = random.Next(minLength, maxLength);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++)
{
double choice = random.NextDouble();
if (choice < 0.333)
sb.Append((char)random.Next('A', 'Z'));
else if (choice < 0.666)
sb.Append((char)random.Next('a', 'z'));
else
sb.Append((char)random.Next('0', '9'));
}
return sb.ToString();
}

生成随机字符串数组:

public static string[] GetRandomArrayString(int n, int minLength, int maxLength)
{
string[] result = new string[n];
for (int i = 0; i < n; i++)
{
result[i] = GetRandomString(minLength, maxLength);
}
return result;
}

测试方法:

public static long Performance(IST<string, int> st, int n, int averageHit)
{
string[] keys = GetRandomArrayString(n, 2, 50);
string keyNotExist = GetRandomString(51, 52);
Stopwatch sw = Stopwatch.StartNew();
// 构建
for (int i = 0; i < n; i++)
st.Put(keys[i], i);
// 查询
for (int i = 0; i < averageHit; i++)
{
for (int j = 0; j < n; j++)
{
st.Get(keys[j]);
st.Get(keyNotExist);
}
}
sw.Stop();
return sw.ElapsedMilliseconds;
}
另请参阅

SymbolTable 库

3.1.32

解答

编码实现即可,实验结果如下:

算法(第四版)C# 习题题解——3.1

对于保持键有序的 BinarySearchST 来说,逆序输入是最坏情况,顺序输入则是最好情况。

而对于键无序的 SequentialSearchST 来说,输入顺序对于性能的影响不大。

只有一种键的时候,每次 Put 都只需要比较一次,值一直在被替换。

只有两种值对性能的影响不大,性能主要由输入的键决定。

代码

测试方法,IST 代表一个符号表。

static void Test(IST<string, int>[] sts, int n)
{
Stopwatch sw = new Stopwatch();
string[] data = SearchCompare.GetRandomArrayString(n, 3, 10);
string item1 = "item1";
Array.Sort(data); // 有序的数组
Console.Write("Sorted Array: ");
sw.Start();
for (int i = 0; i < n; i++)
{
sts[0].Put(data[i], i);
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds); // 逆序的数组
Console.Write("Sorted Array Reversed: ");
sw.Restart();
for (int i = n - 1; i >= 0; i--)
{
sts[1].Put(data[i], i);
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds); // 只有一种键
Console.Write("One Distinct Key: ");
sw.Restart();
for (int i = 0; i < n; i++)
{
sts[2].Put(item1, i);
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds); // 只有两种值
Console.Write("Two Distinct Values: ");
sw.Restart();
for (int i = 0; i < n; i++)
{
sts[3].Put(data[i], i % 2);
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
另请参阅

SymbolTable 库

3.1.33

解答

概率分布的实现方式:

假设存有键的数组为 keys,对 keys 排序。

然后再建立一个长度为 10N 的数组 querys

前 1/2 置为 keys[0],1/2 到 3/4 置为 keys[1],以此类推,直到数组填满。

然后遍历 query 数组,对符号表进行 Get() 操作。

实验结果如下:

算法(第四版)C# 习题题解——3.1

代码
static void Main(string[] args)
{
int n = 1000;
int multiplyBy10 = 3;
for (int i = 0; i < multiplyBy10; i++)
{
Console.WriteLine("n=" + n);
// 构造表
BinarySearchST<string, int> bst = new BinarySearchST<string, int>(n);
MoveToFrontArrayST<string, int> mst = new MoveToFrontArrayST<string, int>(n);
string[] keys = SearchCompare.GetRandomArrayString(n, 3, 20);
for (int j = 0; j < n; j++)
{
bst.Put(keys[j], j);
mst.Put(keys[j], j);
}
// 构造查询
Array.Sort(keys);
string[] querys = new string[10 * n];
int queryIndex = 0, keyIndex = 0;
while (queryIndex < querys.Length)
{
int searchTimes = (int)Math.Ceiling((Math.Pow(0.5, keyIndex + 1) * querys.Length)); for (int j = 0; j < searchTimes && queryIndex < querys.Length; j++)
{
querys[queryIndex++] = keys[keyIndex];
}
keyIndex++;
}
Shuffle(querys); Stopwatch sw = new Stopwatch();
// 测试 MoveToFrontArrayST
sw.Start();
for (int j = 0; j < querys.Length; j++)
{
mst.Get(querys[j]);
}
sw.Stop();
Console.WriteLine("MoveToFrontArrayST: " + sw.ElapsedMilliseconds); // 测试 BinarySearchST
sw.Restart();
for (int j = 0; j < querys.Length; j++)
{
bst.Get(querys[j]);
}
sw.Stop();
Console.WriteLine("BinarySearchST: " + sw.ElapsedMilliseconds); n *= 10;
}
} static void Shuffle<T>(T[] data)
{
for (int i = 0; i < data.Length; i++)
{
int r = i + random.Next(data.Length - i);
T temp = data[r];
data[r] = data[i];
data[i] = temp;
}
}
另请参阅

SymbolTable 库

3.1.34

解答

在上一题的基础上进行修改即可,链接:{% post_link 3.1.33 %}。

调和级数 $ H_n = 1+\frac{1}{2}+\frac{1}{3} + \cdots+\frac{1}{n} $ 。

查询数组变为前 1/2 为 key[0],随后的 1/3 为 key[1],以此类推。

和上一题中的序列进行比较即可,注意删除最后的打乱步骤。

实验结果如下:

算法(第四版)C# 习题题解——3.1

代码

首先建立一个数组计算调和级数,就像这样:

// 调和级数
double[] harmonicNumber = new double[1000 * (int)Math.Pow(10, 4)];
harmonicNumber[0] = 1;
for (int i = 1; i < harmonicNumber.Length; i++)
{
harmonicNumber[i] = harmonicNumber[i - 1] + 1 / (i + 1);
}

然后修改构造查询的代码:

// 构造查询
Array.Sort(keys);
string[] queryZipf = new string[10 * n];
int queryIndex = 0, keyIndex = 0;
while (queryIndex < queryZipf.Length)
{
int searchTimes = (int)Math.Ceiling(queryZipf.Length / (harmonicNumber[keyIndex + 1] * (i + 1))); for (int j = 0; j < searchTimes && queryIndex < queryZipf.Length; j++)
{
queryZipf[queryIndex++] = keys[keyIndex];
}
keyIndex++;
}
另请参阅

SymbolTable 库

3.1.35

解答

实验结果:

算法(第四版)C# 习题题解——3.1

由于包含重复单词,因此结果会比 4 略低一些。

需要对 FrequencyCounter 做一些修改,令其只取前 n 个单词。

代码
static void Main(string[] args)
{
int n = 8000;
int multiplyBy2 = 5;
int repeatTimes = 5;
double lastTime = -1;
Console.WriteLine("n\ttime\tratio");
for (int i = 0; i < multiplyBy2; i++)
{
Console.Write(n + "\t");
long timeSum = 0;
for (int j = 0; j < repeatTimes; j++)
{
SequentialSearchST<string, int> st = new SequentialSearchST<string, int>();
Stopwatch sw = Stopwatch.StartNew();
FrequencyCounter.MostFrequentlyWord("tale.txt", n, 0, st);
sw.Stop();
timeSum += sw.ElapsedMilliseconds;
}
timeSum /= repeatTimes;
Console.Write(timeSum + "\t");
if (lastTime < 0)
Console.WriteLine("--");
else
Console.WriteLine(timeSum / lastTime);
lastTime = timeSum;
n *= 2;
}
}
另请参阅

SymbolTable 库

3.1.36

解答

实验结果如下,增长级为 O(N) ,但速度很快。

算法(第四版)C# 习题题解——3.1

其实只要列出《双城记》不同长度的单词数目,原因就一目了然了。

算法(第四版)C# 习题题解——3.1

大部分单词都集中在中间长度,因此大部分访问也集中在数组中部。

二分查找在访问数组中部的元素时速度很快,因此结果好于预期。

代码
static void Main(string[] args)
{
int n = 8000;
int multiplyBy2 = 5;
int repeatTimes = 5;
double lastTime = -1;
Console.WriteLine("n\ttime\tratio");
for (int i = 0; i < multiplyBy2; i++)
{
Console.Write(n + "\t");
long timeSum = 0;
for (int j = 0; j < repeatTimes; j++)
{
BinarySearchST<string, int> st = new BinarySearchST<string, int>();
Stopwatch sw = Stopwatch.StartNew();
FrequencyCounter.MostFrequentlyWord("tale.txt", n, 0, st);
sw.Stop();
timeSum += sw.ElapsedMilliseconds;
}
timeSum /= repeatTimes;
Console.Write(timeSum + "\t");
if (lastTime < 0)
Console.WriteLine("--");
else
Console.WriteLine(timeSum / lastTime);
lastTime = timeSum;
n *= 2;
}
}
另请参阅

SymbolTable 库

3.1.37

解答

实验结果如下:

算法(第四版)C# 习题题解——3.1

M=10 的时候随机的数字集中在 1024 到 2048 之间,重复值较多,因此 Put 耗时较少。

随着重复值的减少 Put 的耗时会大幅度提高,和实验结果显示的一样。

M=20 的时候数字在 1048576~2097152 之间随机,基本上没有重复值了。

M=30 的时候和 M=20 的情况类似,都是重复值几乎没有的情况。

随机数可以通过如下的方式产生:

result[i] = min + (long)(random.NextDouble() * (max - min));
代码

这里构造了 BinarySearchSTAnalysis 类,在类中声明了两个 Stopwatch 对象,

一个在 Put 方法的开始和结束部分进行计时,

另一个在 Get 方法的开始和结束部分进行计时。

static void Main(string[] args)
{
int n = 1000000;
int m = 10;
int addBy10 = 3; for (int i = 0; i < addBy10; i++)
{
BinarySearchSTAnalysis<long, int> bst = new BinarySearchSTAnalysis<long, int>(n);
long[] data = SearchCompare.GetRandomArrayLong(n, (long)Math.Pow(2, m), (long)Math.Pow(2, m + 1));
FrequencyCounter.MostFrequentlyKey(bst, data);
Console.WriteLine("m=" + m + "\t" + bst.GetTimer.ElapsedMilliseconds + "\t" + bst.PutTimer.ElapsedMilliseconds + "\t" + bst.PutTimer.ElapsedMilliseconds / (double)bst.GetTimer.ElapsedMilliseconds);
m += 10;
} BinarySearchSTAnalysis<string, int> st = new BinarySearchSTAnalysis<string, int>();
FrequencyCounter.MostFrequentlyWord("tale.txt", 0, st);
Console.WriteLine("tales\t" + st.GetTimer.ElapsedMilliseconds + "\t" + st.PutTimer.ElapsedMilliseconds + "\t" + st.PutTimer.ElapsedMilliseconds / (double)st.GetTimer.ElapsedMilliseconds);
Console.ReadLine();
}
另请参阅

SymbolTable 库

3.1.38

解答

实验结果如下:

BinarySearchST

算法(第四版)C# 习题题解——3.1

SequentialSearchST

算法(第四版)C# 习题题解——3.1

对于 BinarySearchST ,每次比较之后以及移动元素时令 Cost 增加。

对于 SequentialSearchST,统计每次的查找次数即可。

然后绘制成散点图即可。

代码

有关绘图的函数,传入的参数为第 iPut() 的开销。

public void Draw(int[] data)
{
Graphics panel = this.CreateGraphics();
float unitX = (float)this.ClientRectangle.Width / data.Length;
float unitY = (float)this.ClientRectangle.Height / data.Max(); int accumulation = 0;
for (int i = 0; i < data.Length; i++)
{
// Gray
panel.FillEllipse(Brushes.Gray, (i + 1) * unitX, this.ClientRectangle.Bottom - data[i] * unitY, 2, 2);
// Red
panel.FillEllipse(Brushes.Red, (i + 1) * unitX, this.ClientRectangle.Bottom - accumulation / (i + 1) * unitY, 2, 2);
accumulation += data[i];
}
}
另请参阅

SymbolTable 库

3.1.39

解答

实验结果如下:

BinarySearchST

算法(第四版)C# 习题题解——3.1

SequentialSearchST

算法(第四版)C# 习题题解——3.1

图像分为两段,分别代表不断向符号表中加入单词和寻找频率最大的单词两个部分。

第一段两个图像的形状类似(注意它们的 y 轴比例不同)。

第二段中 BinarySearchST 的表现要比 SequentialSearchST 稳定的多。

代码

绘图部分代码:

public void Draw(int[] x, long[] y)
{
Graphics panel = this.CreateGraphics(); float unitX = (float)this.ClientRectangle.Width / x.Max();
float unitY = (float)this.ClientRectangle.Height / y.Max(); for (int i = 0; i < x.Length; i++)
{
panel.FillEllipse(
Brushes.Black,
x[i] * unitX,
this.ClientRectangle.Height - y[i] * unitY,
2, 2);
}
}
另请参阅

SymbolTable 库

3.1.40

解答

顺序查找平均需要进行 $ N/2 $ 次比较,二分查找则是 $ \lg N $ 次。

列出方程可以解出 N 的大小

\[\begin {eqnarray*}
1000 \log_2 N &=& N / 2 \\
\log_2 N &=& N / 2000\\
\frac{\ln N}{\ln 2} &=& N/2000 \\
N &=& e^{\frac{\ln 2}{2000}N}\\
1 &=& Ne^{-\frac{\ln 2}{2000}N}\\
N_1 = e^{-W(-\frac{\ln 2}{2000})}=1 &\ & N_2= e^{-W_{-1}(-\frac{\ln 2}{2000})}=29718\\ \\
\end {eqnarray*}
\]

这个方程是一个超越方程,最后的结果中出现了朗伯 W 函数。

同理可以求得 10000 倍的 N=369939。

实验结果如下:

算法(第四版)C# 习题题解——3.1

由于存在缓存优化,每次比较的耗时并不相同。

因此实际耗时并未达到预期,但比较次数是符合预期的。

另请参阅

朗伯 W 函数-*

3.1.41

解答

英文版描述为 1, 2, and 10 times faster。

即一样快,快一倍和快十倍(一个例子)。

和上题类似,也是解超越方程。

插值查找的平均查找次数为 $ \lg(\lg(N)) $。

可以解得 N = 1, 4, 58。

实验结果如下:

算法(第四版)C# 习题题解——3.1

由于 N 太小,可以看到插值查找的运行时间几乎没有变化。