線性表
線性表是最簡單、最基本、最常用的數據結構。線性表是線性結構的抽象(Abstract),線性結構的特點是結構中的數據元素之間存在一對一的線性關係。這種一對一的關係指的是數據元素之間的位置關係,即:
- 除第一個位置的數據元素外,其它數據元素位置的前面都只有一個數據元素;
- 除最後一個位置的數據元素外,其它數據元素位置的後面都只有一個元素。也就是說,數據元素是一個接一個的排列。因此,可以把線性表想象爲一種數據元素序列的數據結構。
線性表就是位置有先後關係,一個接着一個排列的數據結構。
CLR中的線性表
c# 1.1 提供了一個非泛型接口IList接口,接口中的項是object,實現了IList解釦子的類有ArrayList,ListDictionary,StringCollection,StringDictionary.
c# 2.0 提供了泛型的IList接口,實現了List接口的類有List
線性表的接口定義
interface IListDS<T>
{
int GetLength(); //求長度
void Clear(); //清空操作
bool IsEmpty();//判斷線性表是否爲空
void Add(T item);//附加操作
void Insert(T item, int index); //插入操作
T Delete(int index); //刪除操作
T this[int index] { get; }//定義一個索引器 獲取元素
T GetEle(int index);//取表元
int Locate(T value);//按值查找
}
線性表的實現方式
線性表的實現方式有下面幾種
- 順序表
- 單鏈表
- 雙向鏈表
- 循環鏈表
順序表
在計算機內,保存線性表最簡單、最自然的方式,就是把表中的元素一個接一個地放進順序的存儲單元,這就是線性表的順序存儲(Sequence Storage)。線性表的順序存儲是指在內存中用一塊地址連續的空間依次存放線性表的數據元素,用這種方式存儲的線性表叫順序表(Sequence List),如圖所示。順序表的特點是表中相鄰的數據元素在內存中存儲位置也相鄰。
順序表的存儲
假設順序表中的每個數據元素佔w個存儲單元,設第i個數據元素的存儲地址爲Loc(ai),則有:
Loc(ai)= Loc(a1)+(i-1)*w 1≤i≤n式中的Loc(a1)表示第一個數據元素a1的存儲地址,也是順序表的起始存儲地址,稱爲順序表的基地址(Base Address)。也就是說,只要知道順序表的基地址和每個數據元素所佔的存儲單元的個數就可以求出順序表中任何一個數據元素的存儲地址。並且,由於計算順序表中每個數據元素存儲地址的時間相同,所以順序表具有任意存取的特點。(可以在任意位置存取東西)
C#語言中的數組在內存中佔用的存儲空間就是一組連續的存儲區域,因此,數組具有任意存取的特點。所以,數組天生具有表示順序表的數據存儲區域的特性。
順序表的實現
class SeqList<T> : IListDS<T>
{
private T[] data;//用來存儲數據
private int count = 0;
public SeqList(int size)
{
data = new T[size];
}
public SeqList() : this(10)
{
}
public T this[int index]
{
get
{
return GetEle(index);
}
}
public void Add(T item)
{
if (count == data.Length)//當前數組已經存滿
{
Console.WriteLine("當前順序表已存滿,不允許再存入");
}
else
{
data[count] = item;
count++;
}
}
public void Clear()
{
count = 0;
}
public T GetEle(int index)
{
if (index >= 0 && index <= count - 1)
{
return data[index];
}
else
{
Console.WriteLine("超出順序表索引範圍");
return default(T);
}
}
public int GetLength()
{
return count;
}
public void Insert(T item, int index)
{
for (int i = count - 1; i >= index; i--)
{
data[i + 1] = data[i];
}
data[index] = item;
count++;
}
public T Delete(int index)
{
T temp = data[index];
for (int i = index + 1; i < count; i++)
{
data[i - 1] = data[i];
}
count--;
return temp;
}
public bool IsEmpty()
{
return count == 0;
}
public int Locate(T value)
{
for (int i = 0; i < count; i++)
{
if (data[i].Equals(value))
{
return i;
}
}
return -1;
}
}
單鏈表
順序表是用地址連續的存儲單元順序存儲線性表中的各個數據元素,邏輯上相鄰的數據元素在物理位置上也相鄰。因此,在順序表中查找任何一個位置上的數據元素非常方便,這是順序存儲的優點。但是,在對順序表進行插入和刪除時,需要通過移動數據元素來實現,影響了運行效率。線性表的另外一種存儲結構——鏈式存儲(Linked Storage),這樣的線性表叫鏈表(Linked List)。鏈表不要求邏輯上相鄰的數據元素在物理存儲位置上也相鄰,因此,在對鏈表進行插入和刪除時不需要移動數據元素,但同時也失去了順序表可隨機存儲的優點。
單鏈表的存儲
鏈表是用一組任意的存儲單元來存儲線性表中的數據元素(這組存儲單元可以是連續的,也可以是不連續的)。那麼,怎麼表示兩個數據元素邏輯上的相鄰關係呢?即如何表示數據元素之間的線性關係呢?爲此,在存儲數據元素時,除了存儲數據元素本身的信息外,還要存儲與它相鄰的數據元素的存儲地址信息。這兩部分信息組成該數據元素的存儲映像(Image),稱爲結點(Node)。把存儲據元素本身信息的域叫結點的數據域(Data Domain),把存儲與它相鄰的數據元素的存儲地址信息的域叫結點的引用域(Reference Domain)。因此,線性表通過每個結點的引用域形成了一根“鏈條”,這就是“鏈表”名稱的由來。
如果結點的引用域只存儲該結點直接後繼結點的存儲地址,則該鏈表叫單鏈表(Singly Linked List)。把該引用域叫 next。單鏈表結點的結構如圖所示,圖中 data 表示結點的數據域。
鏈式存儲結構
下圖是線性表(a1,a2,a3,a4,a5,a6)對應的鏈式存儲結構示意圖。
另外一種表示形式
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-D01GDnI3-1581864714521)(https://s1.ax1x.com/2018/12/26/F2lFwn.jpg)]
單鏈表節點定義
class Node<T>
{
private T data;
private Node<T> next;
public Node()
{
data = default(T);
next = null;
}
public Node(T value)
{
this.data = value;
this.next = null;
}
public Node(T value, Node<T> next)
{
this.data = value;
this.next = next;
}
public Node(Node<T> next)
{
this.next = next;
}
public T Data
{
get { return data; }
set { data = value; }
}
public Node<T> Next
{
get { return next; }
set { next = value; }
}
}
單鏈表實現
class LinkList<T> : IListDS<T>
{
private Node<T> head;
public LinkList()
{
head = null;
}
public T this[int index]
{
get
{
return GetEle(index);
}
}
public void Add(T item)
{
Node<T> newNode = new Node<T>(item);
if (head == null)
{
head = newNode;
}
else
{
Node<T> temp = head;
while (true)
{
if (temp.Next != null)
{
temp = temp.Next;
}
else
{
break;
}
}
temp.Next = newNode;
}
}
public void Clear()
{
head = null;
}
public T Delete(int index)
{
T data = default(T);
if (index == 0)
{
data = head.Data;
head = head.Next;
}
else
{
Node<T> temp = head;
for (int i = 0; i < index - 1; i++)
{
temp = temp.Next;
}
Node<T> preNode = temp;
Node<T> currentNode = temp.Next;
data = currentNode.Data;
Node<T> nextNode = temp.Next.Next;
preNode.Next = nextNode;
}
return data;
}
public T GetEle(int index)
{
Node<T> temp = head;
T data = temp.Data;
if (index == 0)
{
return data = temp.Data;
}
else
{
for (int i = 0; i < index; i++)
{
temp = temp.Next;
}
data = temp.Data;
}
return data;
}
public int GetLength()
{
if (head == null) return 0;
Node<T> temp = head;
int count = 1;
while (true)
{
if (temp.Next != null)
{
count++;
temp = temp.Next;
}
else
{
break;
}
}
return count;
}
public void Insert(T item, int index)
{
Node<T> newNode = new Node<T>(item);
if (index == 0)
{
newNode.Next = head;
head = newNode;
}
else
{
Node<T> temp = head;
for (int i = 0; i < index - 1; i++)
{
temp = temp.Next;
}
Node<T> preNode = temp;
Node<T> currentNode = temp.Next;
preNode.Next = newNode;
newNode.Next = currentNode;
}
}
public bool IsEmpty()
{
return head == null;
}
public int Locate(T value)
{
Node<T> temp = head;
if (temp == null)
{
return -1;
}
else
{
int index = 0;
while (true)
{
if (temp.Data.Equals(value))
{
return index;
}
else
{
if (temp.Next != null)
{
index++;
temp = temp.Next;
}
else
{
break;
}
}
}
return -1;
}
}
}
雙向鏈表
前面介紹的單鏈表允許從一個結點直接訪問它的後繼結點,所以, 找直接後繼結點的時間複雜度是 O(1)。但是,要找某個結點的直接前驅結點,只能從表的頭引用開始遍歷各結點。如果某個結點的 Next 等於該結點,那麼,這個結點就是該結點的直接前驅結點。也就是說,找直接前驅結點的時間複雜度是 O(n), n是單鏈表的長度。當然,我們也可以在結點的引用域中保存直接前驅結點的地址而不是直接後繼結點的地址。這樣,找直接前驅結點的時間複雜度只有 O(1),但找直接後繼結點的時間複雜度是 O(n)。如果希望找直接前驅結點和直接後繼結點的時間複雜度都是 O(1),那麼,需要在結點中設兩個引用域,一個保存直接前驅結點的地址,叫 prev,一個直接後繼結點的地址,叫 next,這樣的鏈表就是雙向鏈表(Doubly Linked List)。雙向鏈表的結點結構示意圖如圖所示。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UNn84VOH-1581864714522)(https://s1.ax1x.com/2018/12/26/Fg5Mhn.png)]
雙向鏈表節點實現
public class DbNode<T>
{
private T data; //數據域
private DbNode<T> prev; //前驅引用域
private DbNode<T> next; //後繼引用域
//構造器
public DbNode(T val, DbNode<T> p)
{
data = val;
next = p;
}
//構造器
public DbNode(DbNode<T> p)
{
next = p;
}
//構造器
public DbNode(T val)
{
data = val;
next = null;
}
//構造器
public DbNode()
{
data = default(T);
next = null;
}
//數據域屬性
public T Data
{
get { return data; }
set { data = value; }
}
//前驅引用域屬性
public DbNode<T> Prev
{
get { return prev; }
set { prev = value; }
}
//後繼引用域屬性
public DbNode<T> Next
{
get { return next; }
set { next = value; }
}
}
雙向鏈表插入示意圖
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yCTPlLtM-1581864714522)(https://s1.ax1x.com/2018/12/26/Fg5B1x.png)]
循環鏈表
有些應用不需要鏈表中有明顯的頭尾結點。在這種情況下,可能需要方便地從最後一個結點訪問到第一個結點。此時,最後一個結點的引用域不是空引用,而是保存的第一個結點的地址(如果該鏈表帶結點,則保存的是頭結點的地址),也就是頭引用的值。帶頭結點的循環鏈表(Circular Linked List)如圖所示。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Oihv4m5h-1581864714523)(https://s1.ax1x.com/2018/12/26/Fg52AH.png)]
棧和隊列
棧和隊列是非常重要的兩種數據結構,在軟件設計中應用很多。棧和隊列也是線性結構,線性表、棧和隊列這三種數據結構的數據元素以及數據元素間的邏輯關係完全相同,差別是線性表的操作不受限制,而棧和隊列的操作受到限制。
棧的操作只能在表的一端進行,隊列的插入操作在表的一端進行而其它操作在表的另一端進行,所以,把棧和隊列稱爲操作受限的線性表。
棧
棧(Stack)是操作限定在表的尾端進行的線性表。表尾由於要進行插入、刪除等操作,所以,它具有特殊的含義,把表尾稱爲棧頂( Top),另一端是固定的,叫棧底( Bottom)。當棧中沒有數據元素時叫空棧(Empty Stack)。
棧通常記爲: S= (a1,a2,…,an),S是英文單詞stack的第 1 個字母。a1爲棧底元素,an爲棧頂元素。這n個數據元素按照a1,a2,…,an的順序依次入棧,而出棧的次序相反,an第一個出棧,a1最後一個出棧。所以,棧的操作是按照後進先出(Last In First Out,簡稱LIFO)或先進後出(First In Last Out,簡稱FILO)的原則進行的,因此,棧又稱爲LIFO表或FILO表。棧的操作示意圖如圖所示。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-y4ib2xIf-1581864714523)(https://s1.ax1x.com/2018/12/26/FggUzD.png)]
BCL中的棧
C#2.0 一下版本只提供了非泛型的Stack類(存儲object類型)
C#2.0 提供了泛型的Stack類
重要的方法如下:
- Push()入棧(添加數據)
- Pop()出棧(刪除數據,返回被刪除的數據)
- Peek()取得棧頂的數據,不刪除
- Clear()清空所有數據
- Count取得棧中數據的個數
棧的接口定義
public interface IStackDS<T>
{
int Count { get; }
int GetLength(); //求棧的長度
bool IsEmpty(); //判斷棧是否爲空
void Clear(); //清空操作
void Push(T item); //入棧操作
T Pop(); //出棧操作
T Peek(); //取棧頂元素
}
棧的存儲和代碼實現
順序棧
用一片連續的存儲空間來存儲棧中的數據元素(使用數組),這樣的棧稱爲順序棧(Sequence Stack)。類似於順序表,用一維數組來存放順序棧中的數據元素。棧頂指示器 top 設在數組下標爲 0 的端, top 隨着插入和刪除而變化,當棧爲空時,top=-1。下圖是順序棧的棧頂指示器 top 與棧中數據元素的關係圖。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ngxwFMDC-1581864714524)(https://s1.ax1x.com/2018/12/26/FgRZgU.png)]
class SeqStack<T> : IStackDS<T>
{
private T[] data;
private int top;
public SeqStack(int size)
{
data = new T[size];
top = -1;
}
public SeqStack() : this(10)
{
}
public int Count
{
get
{
return top + 1;
}
}
public void Clear()
{
top = -1;
}
public int GetLength()
{
return Count;
}
public bool IsEmpty()
{
return Count == 0;
}
public T Peek()
{
return data[top];
}
public T Pop()
{
T temp = data[top];
top--;
return temp;
}
public void Push(T item)
{
data[top + 1] = item;
top++;
}
}
鏈棧
棧的另外一種存儲方式是鏈式存儲,這樣的棧稱爲鏈棧(Linked Stack)。鏈棧通常用單鏈表來表示,它的實現是單鏈表的簡化。所以,鏈棧結點的結構與單鏈表結點的結構一樣。由於鏈棧的操作只是在一端進行,爲了操作方便,把棧頂設在鏈表的頭部,並且不需要頭結點。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5nlAdDJY-1581864714524)(https://s1.ax1x.com/2018/12/26/FgRvI1.png)]
鏈棧結點實現
鏈棧結點代碼實現:
class Node<T>
{
private T data;
private Node<T> next;
public Node()
{
this.data = default(T);
this.next = null;
}
public Node(T data)
{
this.data = data;
this.next = null;
}
public Node(T value, Node<T> next)
{
this.data = value;
this.next = next;
}
public Node(Node<T> next)
{
this.data = default(T);
this.next = next;
}
public T Data
{
set { data = value; }
get { return data; }
}
public Node<T> Next
{
set { next = value; }
get { return next; }
}
}
鏈棧代碼實現
把鏈棧看作一個泛型類,類名爲 LinkStack。 LinkStack類中有一個字段 top 表示棧頂指示器。由於棧只能訪問棧頂的數據元素,而鏈棧的棧頂指示器又不能指示棧的數據元素的個數。所以,求鏈棧的長度時,必須把棧中的數據元素一個個出棧,每出棧一個數據元素,計數器就增加 1,但這樣會破壞棧的結構。爲保留棧中的數據元素,需把出棧的數據元素先壓入另外一個棧,計算完長度後,再把數據元素壓入原來的棧。但這種算法的空間複雜度和時間複雜度都很高,所以,以上兩種算法都不是理想的解決方法。理想的解決方法是 LinkStack類增設一個字段 num 表示鏈棧中結點的個數。
class LinkStack<T> : IStackDS<T>
{
private Node<T> top;
private int count = 0;
public int Count
{
get
{
return count;
}
}
public void Clear()
{
count = 0;
top = null;
}
public int GetLength()
{
return count;
}
public bool IsEmpty()
{
return count == 0;
}
public T Peek()
{
return top.Data;
}
public T Pop()
{
T data = top.Data;
top = top.Next;
count--;
return data;
}
public void Push(T item)
{
Node<T> temp = new Node<T>(item);
temp.Next = top;
top = temp;
count++;
}
}
隊列
隊列(Queue)是插入操作限定在表的尾部而其它操作限定在表的頭部進行的線性表。把進行插入操作的表尾稱爲隊尾(Rear),把進行其它操作的頭部稱爲隊頭(Front)。當隊列中沒有數據元素時稱爲空隊列(Empty Queue)。
隊列通常記爲: Q= (a1,a2,…,an),Q是英文單詞queue的第 1 個字母。a1爲隊頭元素,an爲隊尾元素。這n個元素是按照a1,a2,…,an的次序依次入隊的,出對的次序與入隊相同,a1第一個出隊,an最後一個出隊。所以,對列的操作是按照先進先出(First In First Out)或後進後出( Last In Last Out)的原則進行的,因此,隊列又稱爲FIFO表或LILO表。隊列Q的操作示意圖如圖所示。
在實際生活中有許多類似於隊列的例子。比如,排隊取錢,先來的先取,後來的排在隊尾。
隊列的操作是線性表操作的一個子集。隊列的操作主要包括在隊尾插入元素、在隊頭刪除元素、取隊頭元素和判斷隊列是否爲空等。與棧一樣,隊列的運算是定義在邏輯結構層次上的,而運算的具體實現是建立在物理存儲結構層次上的。因此,把隊列的操作作爲邏輯結構的一部分,每個操作的具體實現只有在確定了隊列的存儲結構之後才能完成。隊列的基本運算不是它的全部運算,而是一些常用的基本運算。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FX5KlOEk-1581864714524)(https://s1.ax1x.com/2018/12/26/FgfCYq.png)]
BCL 中的隊列
C#2.0 以下版本提供了非泛型的Queue類
C#2.0 提供了泛型Queue類
方法:
- Enqueue()入隊(放在隊尾)
- Dequeue()出隊(移除隊首元素,並返回被移除的元素)
- Peek()取得隊首的元素,不移除
- Clear()清空元素
屬性 - Count獲取隊列中元素的個數
隊列接口定義
interface IQueue<T>
{
int Count { get; }
int GetLeng();
bool IsEmpty();
void Clear();
void Enqueue(T item);
T Dequeue();
T Peek();
}
隊列的存儲和代碼實現
順序隊列
用一片連續的存儲空間來存儲隊列中的數據元素,這樣的隊列稱爲順序隊列(Sequence Queue)。類似於順序棧,用一維數組來存放順序隊列中的數據元素。隊頭位置設在數組下標爲 0 的端,用 front 表示;隊尾位置設在數組的另一端,用 rear 表示。 front 和 rear 隨着插入和刪除而變化。當隊列爲空時, front=rear=-1。
圖是順序隊列的兩個指示器與隊列中數據元素的關係圖。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gGCIaKwp-1581864714525)(https://s1.ax1x.com/2018/12/26/FgfEXF.png)]
class SeqQueue<T> : IQueue<T>
{
private T[] data;
private int count;//數量
private int rear;//隊尾
private int front;//隊首
public SeqQueue(int size)
{
data = new T[size];
count = 0;
rear = front = -1;
}
public SeqQueue() : this(10)
{
}
public int Count
{
get
{
return count;
}
}
public void Clear()
{
count = 0;
rear = front = -1;
}
public T Dequeue()
{
if (count > 0)
{
T temp = data[front + 1];
front++;
count--;
return temp;
}
else
{
Console.WriteLine("隊列爲空,無法取得隊首數據。");
return default(T);
}
}
public void Enqueue(T item)
{
if (count == data.Length)
{
Console.WriteLine("隊列已滿,不可以在添加數據");
}
else
{
if (rear == data.Length - 1)
{
data[0] = item;
rear = 0;
count++;
}
else
{
data[rear + 1] = item;
rear++;
count++;
}
}
}
public int GetLeng()
{
return count;
}
public bool IsEmpty()
{
return count == 0;
}
public T Peek()
{
T temp = data[front + 1];
return temp;
}
}
循環順序隊列
如果再有一個數據元素入隊就會出現溢出。但事實上隊列中並未滿,還有空閒空間,把這種現象稱爲“假溢出”。這是由於隊列“隊尾入隊頭出”的操作原則造成的。解決假溢出的方法是將順序隊列看成是首尾相接的循環結構,頭尾指示器的關係不變,這種隊列叫循環順序隊列(Circular sequence Queue)。循環隊列如圖所示。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GKnRoYFl-1581864714525)(https://s1.ax1x.com/2018/12/26/Fgfc7j.png)]
把循環順序隊列看作是一個泛型類,類名叫 CSeqStack,“ C”是英文單詞 circular 的第 1 個字母。 CSeqStack類實現了接口 IQueue。用數組來存儲循環順序隊列中的元素,在 CSeqStack類中用字段 data 來表示。用字段maxsize 表示循環順序隊列的容量, maxsize 的值可以根據實際需要修改,這通過CSeqStack類的構造器中的參數 size 來實現,循環順序隊列中的元素由 data[0]開始依次順序存放。字段 front 表示隊頭, front 的範圍是 0 到 maxsize-1。字段 rear表示隊尾,rear 的範圍也是 0 到 maxsize-1。如果循環順序隊列爲空,front=rear=-1。當執行入隊列操作時需要判斷循環順序隊列是否已滿,如果循環順序隊列已滿,(rear + 1) % maxsize==front , 循 環 順 序 隊 列 已 滿 不 能 插 入 元 素 。 所 以 ,CSeqStack類除了要實現接口 IQueue中的方法外,還需要實現判斷循環順序隊列是否已滿的成員方法。
鏈隊列
隊列的另外一種存儲方式是鏈式存儲,這樣的隊列稱爲鏈隊列(Linked Queue)。同鏈棧一樣,鏈隊列通常用單鏈表來表示,它的實現是單鏈表的簡化。所以,鏈隊列的結點的結構與單鏈表一樣,如圖所示。由於鏈隊列的操作只是在一端進行,爲了操作方便,把隊頭設在鏈表的頭部,並且不需要頭結點。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-x6cYEpNA-1581864714526)(https://s1.ax1x.com/2018/12/26/FgfqE9.png)]
鏈隊列結點類
class Node<T>
{
private T data;
private Node<T> next;
public Node(T data)
{
this.data = data;
}
public T Data
{
set { data = value; }
get { return data; }
}
public Node<T> Next
{
set { next = value; }
get { return next; }
}
}
鏈隊列代碼實現
把鏈隊列看作一個泛型類,類名爲 LinkQueue。 LinkQueue類中有兩個字段 front 和 rear,表示隊頭指示器和隊尾指示器。由於隊列只能訪問隊頭的數據元素,而鏈隊列的隊頭指示器和隊尾指示器又不能指示隊列的元素個數,所以,與鏈棧一樣,在 LinkQueue類增設一個字段 num 表示鏈隊列中結點的個數。
class LinkQueue<T> : IQueue<T>
{
private Node<T> front;
private Node<T> rear;
private int count;
public LinkQueue()
{
front = rear = null;
count = 0;
}
public int Count
{
get { return count; }
}
public void Clear()
{
count = 0;
rear = front = null;
}
public T Dequeue()
{
if (count == 0)
{
Console.WriteLine("隊列爲空,無法出隊");
return default(T);
}
else if (count == 1)
{
T temp = front.Data;
front = rear = null;
count = 0;
return temp;
}
else
{
T temp = front.Data;
front = front.Next;
count--;
return temp;
}
}
public void Enqueue(T item)
{
Node<T> temp = new Node<T>(item);
if (count == 0)
{
front = rear = temp;
count = 1;
}
else
{
rear.Next = temp;
rear = temp;
count++;
}
}
public int GetLeng()
{
return count;
}
public bool IsEmpty()
{
return count == 0;
}
public T Peek()
{
return front.Data;
}
}
棧和隊列的應用舉例
編程判斷一個字符串是否是迴文。迴文是指一個字符序列以中間字符爲基準兩邊字符完全相同,如字符序列“ ACBDEDBCA”是迴文。
算法思想:判斷一個字符序列是否是迴文,就是把第一個字符與最後一個字符相比較,第二個字符與倒數第二個字符比較,依次類推,第 i 個字符與第 n-i個字符比較。如果每次比較都相等,則爲迴文,如果某次比較不相等,就不是迴文。因此,可以把字符序列分別入隊列和棧,然後逐個出隊列和出棧並比較出隊列的字符和出棧的字符是否相等,若全部相等則該字符序列就是迴文,否則就不是迴文。
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
string str = Console.ReadLine();
Stack<char> stack = new Stack<char>();
Queue<char> queue = new Queue<char>();
for (int i = 0; i < str.Length; i++)
{
stack.Push(str[i]);
queue.Enqueue(str[i]);
}
bool isHui = true;
while (stack.Count > 0)
{
if (stack.Pop() != queue.Dequeue())
{
isHui = false;
break;
}
}
Console.WriteLine("是否是迴文字符串:" + isHui);
Console.ReadKey();
}
}
串和數組
在應用程序中使用最頻繁的類型是字符串。字符串簡稱串,是一種特殊的線性表,其特殊性在於串中的數據元素是一個個的字符。字符串在計算機的許多方面應用很廣。如在彙編和高級語言的編譯程序中,源程序和目標程序都是字符串數據。在事務處理程序中,顧客的信息如姓名、地址等及貨物的名稱、產地和規格等,都被作爲字符串來處理。另外,字符串還具有自身的一些特性。因此,把字符串作爲一種數據結構來研究。
串
串的基本概念
串(String)由 n(n≥0)字符組成的有限序列。一般記爲:
S=”c1c2…cn” (n≥0)
其中, S是串名,雙引號作爲串的定界符,用雙引號引起來的字符序列是串值。 ci( 1≤i≤n)可以是字母、數字或其它字符, n爲串的長度,當n=0 時,稱爲空串(Empty String)。
串中任意個連續的字符組成的子序列稱爲該串的子串(Substring)。包含子串的串相應地稱爲主串。子串的第一個字符在主串中的位置叫子串的位置。如串s1”abcdefg”,它的長度是 7,串s2”cdef”的長度是 4, s2是s1的子串, s2的位置是 3。
如果兩個串的長度相等並且對應位置的字符都相等,則稱這兩個串相等。而在 C#中,比較兩個串是否相等還要看串的語言文化等信息。
串的存儲和代碼實現
由於串中的字符都是連續存儲的,而在 C#中串具有恆定不變的特性,即字符串一經創建,就不能將其變長、變短或者改變其中任何的字符。所以,這裏不討論串的鏈式存儲,也不用接口來表示串的操作。同樣,把串看作是一個類,類名爲 StringDS。取名爲 StringDS 是爲了和 C#自身的字符串類 String 相區別。類StringDS 只有一個字段,即存放串中字符序列的數組 data。由於串的運算有很多,類 StringDS 中只包含部分基本的運算。串類 StringDS中的方法和屬性:
class StringDS
{
private char[] data;//用來存放字符串
/// <summary>
/// 構造器
/// </summary>
/// <param name="array">字符數組</param>
public StringDS(char[] array)
{
data = new char[array.Length];
for (int i = 0; i < array.Length; i++)
{
data[i] = array[i];
}
}
/// <summary>
/// 構造器
/// </summary>
/// <param name="str">字符串</param>
public StringDS(string str)
{
data = new char[str.Length];
for (int i = 0; i < str.Length; i++)
{
data[i] = str[i];
}
}
/// <summary>
/// 索引器
/// </summary>
/// <param name="index">索引下標</param>
/// <returns></returns>
public char this[int index]
{
get
{
return data[index];
}
}
/// <summary>
/// 獲得串的長度
/// </summary>
/// <returns>長度</returns>
public int GetLength()
{
return data.Length;
}
/// <summary>
/// 如果兩個字符串一樣長,返回0
/// 如果當前字符串小於s,那麼返回-1
/// 如果當前字符串大於s,那麼返回1
/// </summary>
/// <param name="">要比較的串</param>
/// <returns></returns>
public int Compare(StringDS s)
{
int len = this.GetLength() > s.GetLength() ? this.GetLength() : s.GetLength(); //取得較短字符串;
int index = -1;//用來記錄兩個字符串不相同字符的位置;
for (int i = 0; i < len; i++)
{
if (this[i] != s[i])
{
index = i;
break;
}
}
if (index != -1)
{
if (this[index] > s[index])
{
return 1;
}
else
{
return -1;
}
}
else
{
if (this.GetLength() == s.GetLength())
{
return 0;
}
else
{
if (this.GetLength() > s.GetLength())
{
return 1;
}
else
{
return -1;
}
}
}
}
/// <summary>
/// 剪切字符串
/// </summary>
/// <param name="index">剪切點的下標</param>
/// <param name="length">要剪切的長度</param>
/// <returns></returns>
public StringDS SubString(int index, int length)
{
char[] newData = new char[length];
for (int i = index; i < index + length; i++)
{
newData[i - index] = data[i];
}
return new StringDS(newData);
}
/// <summary>
/// 拼接字符串
/// </summary>
/// <param name="s1">要拼接的串1</param>
/// <param name="s2">要拼接的串2</param>
/// <returns></returns>
public static StringDS Concat(StringDS s1, StringDS s2)
{
char[] newData = new char[s1.GetLength() + s2.GetLength()];
for (int i = 0; i < s1.GetLength(); i++)
{
newData[i] = s1[i];
}
for (int i = s1.GetLength(); i < s1.GetLength() + s2.GetLength(); i++)
{
newData[i] = s2[i - s1.GetLength()];
}
return new StringDS(newData);
}
/// <summary>
/// 查找當前串中與串s相同的第一個下標
/// </summary>
/// <param name="s">要在當前串中查找的串</param>
/// <returns></returns>
public int IndexOf(StringDS s)
{
for (int i = 0; i <= this.GetLength() - s.GetLength(); i++)
{
bool isEqual = true;
for (int j = i; j < i + s.GetLength(); j++)
{
if (this[j] != s[j - i])
{
isEqual = false;
}
}
if (isEqual)
{
return i;
}
else
{
continue;
}
}
return -1;
}
/// <summary>
/// 重寫ToString
/// </summary>
/// <returns>返回一個字符串</returns>
public override string ToString()
{
return new string(data);
}
}
C#中的串
在 C#中,一個 String 表示一個恆定不變的字符序列集合。 String 類型是封閉類型,所以,它不能被其它類繼承,而它直接繼承自 object。因此, String 是引用類型,不是值類型,在託管堆上而不是在線程的堆棧上分配空間。 String 類型還 繼 承 了 IComparable 、 ICloneable 、 IConvertible 、 IComparable<string> 、IEnumerable<char>、 IEnumerable 和 IEquatable<string>等接口。 String 的恆定性指的是一個串一旦被創建,就不能將其變長、變短或者改變其中任何的字符。所以,當我們對一個串進行操作時,不能改變字符串,如在本書定義的 StringDS 類中,串連接、串插入和串刪除等操作的結果都是生成了新串而沒有改變原串。 C#也提供了 StringBuilder 類型來支持高效地動態創建字符串。
在 C#中,創建串不能用 new 操作符,而是使用一種稱爲字符串駐留的機制。
這是因爲 C#語言將 String 看作是基元類型。基元類型是被編譯器直接支持的類型,可以在源代碼中用文本常量(Literal)來直接表達字符串。當 C#編譯器對源代碼進行編譯時,將文本常量字符串存放在託管模塊的元數據中。而當 CLR 初始化時, CLR 創建一個空的散列表,其中的鍵是字符串,值爲指向託管堆中字符串對象的引用。散列表就是哈希表。當 JIT編譯器編譯方法時,它會在散列表中查找每一個文本常量字符串。如果找不到,就會在託管堆中構造一個新的 String 對象(指向字符串),然後將該字符串和指向該字符串對象的引用添加到散列表中;如果找到了,不會執行任何操作。
數組
c#中的數組
數組是一種常用的數據結構,可以看作是線性表的推廣。數組作爲一種數據結構,其特點是結構中的數據元素可以是具有某種結構的數據,甚至可以是數組,但屬於同一數據類型。數組在許多高級語言裏面都被作爲固定類型來使用。
數組是 n(n≥1)個相同數據類型的數據元素的有限序列。一維數組可以看作是一個線性表,二維數組可以看作是“數據元素是一維數組”的一維數組,三維數組可以看作是“數據元素是二維數組”的一維數組,依次類推。
C#支持一維數組、多維數組及交錯數組(數組的數組)。所有的數組類型都隱含繼承自System.Array。Array 是一個抽象類,本身又繼承自 System.Object。所以,數組總是在託管堆上分配空間,是引用類型。任何數組變量包含的是一個指向數組的引用,而非數組本身。當數組中的元素的值類型時,該類型所需的內存空間也作爲數組的一部分而分配;當數組的元素是引用類型時,數組包含是隻是引用。
Array類中的常用方法
using System;
using System.Collections;
public abstract class Array : ICloneable, IList, ICollection, IEnumerable
{
//判斷 Array 是否具有固定大小。
public bool IsFixedSize { get; }
//獲取 Array 元素的個數。
public int Length { get; }
//獲取 Array 的秩(維數)。
public int Rank { get; }
//實現的 IComparable 接口,在.Array 中搜索特定元素。
public static int BinarySearch(Array array, object value);
//實現的 IComparable<T>泛型接口,在 Array 中搜索特定元素。
public static int BinarySearch<T>(T[] array, T value);
//實現 IComparable 接口,在 Array 的某個範圍中搜索值。
public static int BinarySearch(Array array, int index, int length, object value);
//實現的 IComparable<T>泛型接口,在 Array 中搜索值。
public static int BinarySearch<T>(T[] array, int index, int length, T value);
//Array 設置爲零、 false 或 null,具體取決於元素類型。
public static void Clear(Array array, int index, int length);
//System.Array 的淺表副本。
public object Clone();
//從第一個元素開始複製 Array 中的一系列元素
//到另一 Array 中(從第一個元素開始)。
public static void Copy(Array sourceArray,
Array destinationArray, int length);
//將一維 Array 的所有元素複製到指定的一維 Array 中。
public void CopyTo(Array array, int index);
//創建使用從零開始的索引、具有指定 Type 和維長的多維 Array。
public static Array CreateInstance(Type elementType, params int[] lengths);
//返回 ArrayIEnumerator。
public IEnumerator GetEnumerator();
//獲取 Array 指定維中的元素數。
public int GetLength(int dimension);
//獲取一維 Array 中指定位置的值。
public object GetValue(int index);
//返回整個一維 Array 中第一個匹配項的索引。
public static int IndexOf(Array array, object value);
//返回整個.Array 中第一個匹配項的索引。
public static int IndexOf<T>(T[] array, T value);
//返回整個一維 Array 中最後一個匹配項的索引。
public static int LastIndexOf(Array array, object value);
//反轉整個一維 Array 中元素的順序。
public static void Reverse(Array array);
//設置給一維 Array 中指定位置的元素。
public void SetValue(object value, int index);
//對整個一維 Array 中的元素進行排序。
public static void Sort(Array array);
}
簡單排序方法
排序
排序(Sort)是計算機程序設計中的一種重要操作,也是日常生活中經常遇到的問題。例如,字典中的單詞是以字母的順序排列,否則,使用起來非常困難。同樣,存儲在計算機中的數據的次序,對於處理這些數據的算法的速度和簡便性而言,也具有非常深遠的意義。
基本概念
排序是把一個記錄(在排序中把數據元素稱爲記錄)集合或序列重新排列成按記錄的某個數據項值遞增(或遞減)的序列。
下表是一個學生成績表,其中某個學生記錄包括學號、姓名及計算機文化基礎、C 語言、數據結構等課程的成績和總成績等數據項。在排序時,如果用總成績來排序,則會得到一個有序序列;如果以數據結構成績進行排序,則會得到另一個有序序列。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7CevmBS6-1581864714526)(https://s1.ax1x.com/2018/12/26/FgXCbq.png)]
作爲排序依據的數據項稱爲“排序項”,也稱爲記錄的關鍵碼(Keyword)。關鍵碼分爲主關鍵碼(Primary Keyword)和次關鍵碼(Secondary Keyword)。一般地,若關鍵碼是主關鍵碼,則對於任意待排序的序列,經排序後得到的結果是唯一的;若關鍵碼是次關鍵碼,排序的結果不一定唯一,這是因爲待排序的序列中可能存在具有相同關鍵碼值的記錄。此時,這些記錄在排序結果中,它們之間的位置關係與排序前不一定保持一致。如果使用某個排序方法對任意的記錄序列按關鍵碼進行排序,相同關鍵碼值的記錄之間的位置關係與排序前一致,則稱此排序方法是穩定的;如果不一致,則稱此排序方法是不穩定的。
由於待排序的記錄的數量不同,使得排序過程中涉及的存儲器不同,可將排序方法分爲內部排序(Internal Sorting)和外部排序(External Sorting)兩大類。
內部排序指的是在排序的整個過程中,記錄全部存放在計算機的內存中,並且在內存中調整記錄之間的相對位置,在此期間沒有進行內、外存的數據交換。外部排序指的是在排序過程中,記錄的主要部分存放在外存中,藉助於內存逐步調整記錄之間的相對位置。在這個過程中,需要不斷地在內、外存之間交換數據。
直接插入排序
插入排序的基本操作就是將一個數據插入到已經排好序的有序數據中,從而得到一個新的、個數加一的有序數據,算法適用於少量數據的排序,時間複雜度爲O(n^2)。是穩定的排序方法。插入算法把要排序的數組分成兩部分:第一部分包含了這個數組的所有元素,但將最後一個元素除外(讓數組多一個空間纔有插入的位置),而第二部分就只包含這一個元素(即待插入元素)。在第一部分排序完成後,再將這個最後元素插入到已排好序的第一部分中。
插入排序的基本思想是:每步將一個待排序的紀錄,按其關鍵碼值的大小插入前面已經排序的文件中適當位置上,直到全部插入完爲止。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ufhWfSCk-1581864714527)(https://s1.ax1x.com/2018/12/26/FgX1IK.png)]
using System;
class Program
{
static void InsertSort(int[] dataArray)
{
for (int i = 1; i < dataArray.Length; i++)
{
int iValue = dataArray[i];
bool isInsert = false;
//拿到i位置的元素 跟前面所有的元素做比較
//如果發現比i大的,就讓它向後移動
for (int j = i - 1; j >= 0; j--)
{
if (dataArray[j] > iValue)
{
dataArray[j + 1] = dataArray[j];
}
else
{
//發現一個比i小的值就不移動了
dataArray[j + 1] = iValue;
isInsert = true;
break;
}
}
if (isInsert == false)
{
dataArray[0] = iValue;
}
}
}
static void Main(string[] args)
{
int[] data = new int[] { 42, 20, 17, 27, 13, 8, 17, 48 };
InsertSort(data);
foreach (var temp in data)
{
Console.Write(temp + " ");
}c
Console.ReadKey();
}
}
冒泡排序
冒泡排序(Bubble Sort)的基本思想是:將相鄰的記錄的關鍵碼進行比較,若前面記錄的關鍵碼大於後面記錄的關鍵碼,則將它們交換,否則不交換。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-z8BCTzaR-1581864714527)(https://s1.ax1x.com/2018/12/26/FgXGGD.png)]
class Program
{
static void Main(string[] args)
{
int temp = 0;
int[] arr = { 23, 44, 66, 76, 98, 11, 3, 9, 7 };
#region 該段與排序無關
Console.WriteLine("排序前的數組:");
foreach (int item in arr)
{
Console.Write(item + "");
}
Console.WriteLine();
#endregion
for (int i = 0; i < arr.Length - 1; i++)
{
#region 將大的數字移到數組的arr.Length-1-i
for (int j = 0; j < arr.Length - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
#endregion
}
Console.WriteLine("排序後的數組:");
foreach (int item in arr)
{
Console.Write(item + "");
}
Console.WriteLine();
Console.ReadKey();
}
}
簡單選擇排序
簡單選擇排序(Simple Select Sort)算法的基本思想是:從待排序的記錄序列中選擇關鍵碼最小(或最大)的記錄並將它與序列中的第一個記錄交換位置;然後從不包括第一個位置上的記錄序列中選擇關鍵碼最小(或最大)的記錄並將它與序列中的第二個記錄交換位置;如此重複,直到序列中只剩下一個記錄爲止。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oDcM6OiN-1581864714527)(https://s1.ax1x.com/2018/12/26/FgX8PO.png)]
using System;
class Program
{
static void SelectSort(int[] dataArray)
{
for (int i = 0; i < dataArray.Length - 1; i++)
{
int min = dataArray[i];
int minIndex = i;//最小值所在索引
for (int j = i + 1; j < dataArray.Length; j++)
{
if (dataArray[j] < min)
{
min = dataArray[j];
minIndex = j;
}
}
if (minIndex != i)
{
int temp = dataArray[i];
dataArray[i] = dataArray[minIndex];
dataArray[minIndex] = temp;
}
}
}
static void Main(string[] args)
{
int[] data = new int[] { 42, 20, 17, 27, 13, 8, 17, 48 };
SelectSort(data);
foreach (var temp in data)
{
Console.Write(temp + " ");
}
Console.ReadKey();
}
}
快速排序
快速排序由於排序效率綜合來說你幾種排序方法中效率較高,因此經常被採用,再加上快速排序思想----分治法也確實實用,因此很多軟件公司的筆試面試,包括像騰訊,微軟等知名IT公司都喜歡考這個,還有大大小的程序方面的考試如軟考,考研中也常常出現快速排序的身影。
快速排序是C.R.A.Hoare於1962年提出的一種劃分交換排序。它採用了一種分治的策略,通常稱其爲分治法(Divide-and-ConquerMethod)。
該方法的基本思想是:
- 先從數列中取出一個數作爲基準數。
- 分區過程,將比這個數大的數全放到它的右邊,小於或等於它的數全放到它的左邊。
- 再對左右區間重複第二步,直到各區間只有一個數。
快速排序詳細步驟
以一個數組作爲示例,取區間第一個數爲基準數。
初始時,i = 0; j = 9; X = a[i] = 72
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lqGdbCB6-1581864714528)(https://s1.ax1x.com/2018/12/26/FgjEwt.png)]
由於已經將a[0]中的數保存到X中,可以理解成在數組a[0]上挖了個坑,可以將其它數據填充到這來。
從j開始向前找一個比X小或等於X的數。當j=8,符合條件,將a[8]挖出再填到上一個坑a[0]中。a[0]=a[8]; i++; 這樣一個坑a[0]就被搞定了,但又形成了一個新坑a[8],這怎麼辦了?簡單,再找數字來填a[8]這個坑。這次從i開始向後找一個大於X的數,當i=3,符合條件,將a[3]挖出再填到上一個坑中a[8]=a[3]; j–;
數組變爲:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nkhzr6nQ-1581864714528)(https://s1.ax1x.com/2018/12/26/FgjAeI.png)]
i = 3; j = 7; X=72
再重複上面的步驟,先從後向前找,再從前向後找。
從j開始向前找,當j=5,符合條件,將a[5]挖出填到上一個坑中,a[3] = a[5]; i++
從i開始向後找,當i=5時,由於i==j退出。
此時,i = j = 5,而a[5]剛好又是上次挖的坑,因此將X填入a[5]。
數組變爲:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-H3fTJ27R-1581864714529)(https://s1.ax1x.com/2018/12/30/FhUB4S.png)]
可以看出a[5]前面的數字都小於它,a[5]後面的數字都大於它。因此再對a[0…4]和a[6…9]這二個子區間重複上述步驟就可以了。
快速排序代碼實現
using System;
class Program
{
/// <summary>
/// 對數組dataArray中索引從left到right之間的數做排序
/// </summary>
/// <param name="dataArray">要排序的數組</param>
/// <param name="left">要排序數據的開始索引</param>
/// <param name="right">要排序數據的結束索引</param>
static void QuickSort(int[] dataArray, int left, int right)
{
if (left < right)
{
int x = dataArray[left];//基準數, 把比它小或者等於它的 放在它的左邊,然後把比它大的放在它的右邊
int i = left;
int j = right;//用來做循環的標誌位
while (true && i < j)//當i==j的時候,說明我們找到了一箇中間位置,這個中間位置就是基準數應該所在的位置
{
//從後往前比較(從右向左比較) 找一個比x小(或者=)的數字,放在我們的坑裏 坑位於i的位置
while (true && i < j)
{
if (dataArray[j] <= x) //找到了一個比基準數 小於或者等於的數子,應該把它放在x的左邊
{
dataArray[i] = dataArray[j];
break;
}
else
{
j--;//向左移動 到下一個數字,然後做比較
}
}
//從前往後(從左向右)找一個比x大的數字,放在我們的坑裏面 現在的坑位於j的位置
while (true && i < j)
{
if (dataArray[i] > x)
{
dataArray[j] = dataArray[i];
break;
}
else
{
i++;
}
}
}
//跳出循環 現在i==j i是中間位置
dataArray[i] = x;// left -i- right
QuickSort(dataArray, left, i - 1);
QuickSort(dataArray, i + 1, right);
}
}
static void Main(string[] args)
{
int[] data = new int[] { 42, 20, 17, 27, 13, 8, 17, 48 };
QuickSort(data, 0, data.Length - 1);
foreach (var temp in data)
{
Console.Write(temp + " ");
}
Console.ReadKey();
}
}
快排總結:
- i =L; j = R; 將基準數挖出形成第一個坑a[i]。
- j–由後向前找比它小的數,找到後挖出此數填前一個坑a[i]中。
- i++由前向後找比它大的數,找到後也挖出此數填到前一個坑a[j]中。
- 再重複執行2,3二步,直到i==j,將基準數填入a[i]中。