1. 程式人生 > >從零開始實現資料結構(二) 有序陣列

從零開始實現資料結構(二) 有序陣列

有序陣列顧名思義就是陣列內所有元素的排列都是按照一定的順序的。不管插入元素是多少,有序陣列都會自動將該元素插入合適的位置。這裡要實現的是一個定長的有序陣列。

一.有序陣列COrderArray類

要實現一個有序陣列,首先應該考慮這個陣列應該要實現什麼樣的功能:
1.建構函式,初始化陣列,給定指定的陣列長度。
2.插入資料,最重要的功能,插入的資料自動按照順序排好。
3.按照指定值查詢,有序陣列有一個很重要的特點就是很方便查詢某一個特定值。
4.按照指定的位置查詢,這個功能也相當於為過載 [] 下標運算子,這樣就可以像普通的陣列一樣通過下標訪問有序陣列的元素。
5.刪除指定位置的元素。
6.刪除指定值的元素。

7.清空所有元素。
8.合併兩個有序陣列,這個功能也是刷演算法題可能會遇到的一種型別的題目。

理清楚大概要實現什麼樣的功能,就可以開始寫標頭檔案的函式宣告。

#pragma once
#include "CArray.h"
class COrderArray ://依然是繼承陣列基類
    public CArray
{
public:
    COrderArray(int size);//建構函式
    void PushBack(int value);//插入資料
    void RemoveForValue(int value);//根據指定值刪除陣列元素
    void RemoveForPos(int pos);//根據指定位置刪除陣列元素
    int FindForValue(int value);//根據指定值查詢陣列元素
    int &operator[](int pos);
    //過載下標運算子,達到按位置運算元組元素的目的
    //過載下標運算子的時候,這裡的返回值必須是引用型別
    void Clear();//清空所有元素
    void Merge(COrderArray& array);//合併兩個有序陣列
private:
    void _InsertElement(int value, int start);
    //因為在陣列中插入值的時候會將該位置後的所有的元素都向後移一個位置,引數為插入值和後移的初始位
    //所以單獨寫一個私有函式來完成插入並後移的功能,防止函式體內容太多,該函式也是不能在外部去呼叫
    void _RemoveElement(int pos);
    //和上面同理,這個函式是用來刪除陣列中某個位置的元素,刪除完成後把該位置後面的元素前移
    
    //注意:這兩個函式和上面的的插入、刪除函式的不同之處在於上面兩個公有函式負責給使用者呼叫
    //並且實現用什麼樣的策略去刪除或者插入,它們是內部呼叫下面的兩個私有函式
    //而這兩個私有函式只是是完成插入和刪除的功能
};

其他的成員函式以及成員變數在CArray陣列基類中已經定義了,這裡就不用再寫。

二.COrderArray類的具體實現

1.建構函式
因為這是一個定長的有序陣列,所以建構函式需要給定一個n,然後把陣列的容量設定為n

COrderArray::COrderArray(int size)
{
    m_Capacity = size;
    mp_Array = new int[m_Capacity];
}

2.插入元素
有序陣列每插入一個元素都要考慮插入的位置,確保插入以後整個陣列依然是有序的。所以插入元素以前要把當前要插入的元素和陣列已經有的元素大小進行比較。可以採用順序比較的方法,即從最小的元素開始比較。

但是因為這是一個有序陣列,順序比較的方式會顯得很笨重,在這裡使用二分法是最好的。即首先確定一個初始位和末尾位,然後兩個位置中間的元素。如果要插入的元素比中間的元素大,則把起始位重新設定為現在的中間位置、,然後重新計算新的中間位,再比較中間位。這樣的好處是很快就能找到合適的插入位置。

//插入元素,判斷元素大小來決定其應該在陣列中位置
void COrderArray::PushBack(int value)
{
    //陣列元素個數已經達到陣列最大容量,直接返回
    if (m_Size == m_Capacity)
    {
        return;
    }

    //二分查詢
    int start = 0;
    int end = m_Size;
    int mid = (start + end) / 2;
    while (1)
    {
        //陣列還沒有元素的時候,直接插入
        if (m_Size == 0)
        {
            mp_Array[m_Size] = value;
            break;
        }
        //迴圈跳出的條件,即mid與首尾之一相等
        if (mid==start||mid==end)
        {
            if (mp_Array[mid] > value)//如果最後一次迴圈時,mid位置的元素大於插入位置的元素
            {
                _InsertElement(value, mid);//對當前mid位置,呼叫插入元素的函式
                break;
            }
            //若最後一次迴圈時,mid位置元素小於插入位置的元素
            //對mid位置後面的一個位置,呼叫插入元素的函式   
            _InsertElementvalue, mid + 1);
            
            break;
        }
        //二分法,比較插入元素和中間元素的大小,然後改變mid的值
        if (mp_Array[mid] > value)//插入元素比中間元素小,end改為mid,mid根據新的end更新一下
        {
            end = mid;
            mid = (start + end) / 2;
        }
        else//插入元素比中間元素大,start改為mid,mid根據新的start更新一下
        {
            start = mid;
            mid = (start + end) / 2;
        }
    }
    m_Size++;
}

二分法的過程比較適合在紙上一步一步畫出來,就很容易理解這個過程。
在剛剛的函式中只是實現了去搜尋插入元素應該插入的位置,實際上還並沒有完成插入,所以在迴圈結束的時候還呼叫了_InsertElement(value, mid),來完成插入,並把插入位置後面的所有元素都後移一位。
**_InsertElement函式如下:**

//在陣列中要插入的元素位置之後的所有元素進行後移
void COrderArray::_InsertElement(int value, int pos)
{
    for (int i = m_Size; i > pos; i--)
    {
        mp_Array[i] = mp_Array[i - 1];
    }
    mp_Array[pos] = value;
}

這裡只需要根據插入的位置,一個for迴圈把後面所有的元素移動一位,再插入新的元素即可。

3.根據指定值查詢元素
根據值查詢元素和上面的方法類似,使用二分查詢。

//根據值查詢,二分查詢,和上面的插入類似
int COrderArray::FindForValue (int value)
{
    int start = 0;
    int end = m_Size;
    int mid = (start + end) / 2;
    while (mid != start && mid != end)
    {
        if (mp_Array[mid] == value)
        {
            return mid;
        }
        if (mp_Array[mid] > value)
        {
            end = mid;
            mid = (start + end) / 2;
        }
        else
        {
            start = mid;
            mid = (start + end) / 2;
        }
    }
    if (mp_Array[mid] == value)
    {
        return mid;
    }
}

4.根據指定位置查詢元素
這一個功能實際上和陣列的下標索引是相同的,所以這個地方可以不寫函式,而是過載下標運算子。之前寫的動態陣列也可以加入這一個功能。

int &COrderArray::operator[](int pos)
{
    if (pos > m_Size || pos < 0)
    {
        return mp_Array[0];
    }
    return mp_Array[pos];
}

過載下標運算子的時候返回值是一個引用型別,而且最好是做一個越界的檢查,如果下標索引越界,就返回陣列的第一個元素。

5.清空所有元素
和動態陣列清空元素類似,只需要把當前元素個數置零即可。

//清空所有元素
void COrderArray::Clear()
{
    m_Size = 0;
}

6.根據指定位置刪除元素
根據指定位置刪除陣列元素只需要把要刪除的位置後面的所有元素都向前移動一個位置,這樣就自動覆蓋了要刪除的元素。而前面寫的_RemoveElement(int pos)函式功能就是將刪除元素,並指定位置後的元素向前移動,所有這裡可以直接考慮呼叫這個函式。
**_RemoveElement(int pos)如下:**

//刪除操作,把刪除的數後面每一個元素向前移動一個位置
void COrderArray::_RemoveElement(int pos)
{
    if (pos >= 0 && pos < m_Size)
    {
        for (int i = pos; i < m_Size; i++)
        {
            mp_Array[i] = mp_Array[i + 1];
        }
        m_Size--;
    }
}

根據指定位置刪除元素的函式:

//根據指定位置刪除元素
void COrderArray::RemoveForPos(int pos)
{
    _RemoveElement(pos);
}

7.根據指定值刪除元素
要根據指定值刪除陣列中的元素,首先其實還是要查詢到陣列中指定值,所以可以直接用前面寫的查詢指定值的函式,用一個變數來接收返回的位置,找到以後再呼叫前面的的_RemoveElement(int pos)函式即可。

//根據指定值刪除元素
void COrderArray::RemoveForValue(int value)
{
    int pos = FindForValue(value);
    _RemoveElement(pos);
}

8.合併兩個有序陣列
要實現兩個有序陣列的合併,有幾種思考方式,一種是使用一個新的物件來裝合併後的有序陣列,這種開銷相對比較大。所以這裡選擇將第一個有序陣列和第二個有序陣列都合併到第一個裡面,並且不改變第二個陣列。所以函式的引數就應該是第二個有序陣列。
即 Merge(COrderArray& tempArray)
注意這裡使用的是COrderArray&,這是一個引用。當類物件做函式形參的時候通常都會使用引用的方式,因為使用引用不會呼叫拷貝建構函式,而是直接操作原來的物件,這樣可以減輕程式的開銷。
同時,如果在物件中包含有new出來的指標,一定得要使用引用的方式傳參。因為值傳遞會生成一個物件的副本,在結束的時候還會自動執行解構函式,會把原來物件new出來的指標delete掉。而在整個程式結束時,又會呼叫一次解構函式,這樣重複delete同一個指標,會執行出錯。而引用作引數則不會生成物件的副本,也不會呼叫解構函式。當然,如果物件本身沒有動態建立的變數,值傳遞也不會出太大的問題。
合併的思想主要是兩個陣列依次比大小,小的就先放進新的陣列中,然後繼續比下去。
因為這裡建立的是一個定長陣列,所以得先開一個新的更大的記憶體空間,大小就為兩個陣列容量之和,所以這裡的合併從後往前合併應該是更好的。

//合併兩個有序陣列
void COrderArray::Merge(COrderArray& tempArray)
{
    //開一個新的記憶體來儲存合併後的陣列,首先需要計算新的記憶體需要多大
    int tempArrayCapacity = tempArray.getCapacity();
    int newCapacity = tempArrayCapacity + m_Capacity;
    int* newSpace = new int[newCapacity];

    //因為原來陣列的元素有可能沒有滿
    //所以要從兩個陣列當前size的位置開始合併
    //tempsize即兩個陣列的size之和
    //pos是合併的時候的一個索引,每取出來一個元素,就放到newSpace[pos],並-1,到0的時候就合併完
    int tempArraySize = tempArray.getSize();
    int tempSize = tempArraySize + m_Size;
    int pos = tempSize - 1;//索引要-1

    //開始合併
    while (pos >= 0)
    {
    //如果第一個陣列元素個數不為0才進行比較
        if (m_Size != 0)
        {
            //第一個陣列元素大於傳參進來的陣列元素
            //或者第二個陣列元素索引已經比較到0
            //第一個陣列的元素放進新開的記憶體空間,然後size-1
            if (mp_Array[m_Size - 1] >= tempArray[tempArraySize - 1])
            {
                newSpace[pos] = mp_Array[m_Size - 1];
                m_Size--;
            }
            else
            {
                newSpace[pos] = tempArray[tempArraySize - 1];
                tempArraySize--;
            }
        }else//如果第一個陣列元素個數已經為0,直接把另一個數組的元素按順序放進來,不需要再進行比較
        {
            newSpace[pos] = tempArray[tempArraySize - 1];
            tempArraySize--;
        }
        pos--;
    }
    //清理掉要合併陣列的原來的記憶體
    //把size設定為新的合併後的size
    //陣列容量設定為新的容量
    //指標要指向新的存放資料的記憶體空間
    delete[] mp_Array;
    m_Size = tempSize;
    m_Capacity = newCapacity;
    mp_Array = newSpace;
}

到這裡,所有的有序陣列的功能就實現完畢,接下來是主函式來呼叫這個有序陣列。

三.主函式Array.cpp

主函式主要是建立兩個有序陣列物件,然後插入資料,可以看到插入的資料都是有序的,然後再合併兩個有序陣列。

#include<iostream>
using namespace std;
#include "COrderArray.h"
using namespace std;
int main()
{
    int n;
    cin >> n;

    //COrderArray* oArray = new COrderArray(n);
    COrderArray oArray(n);
    COrderArray oArray2(n);
    //插入第一個陣列的資料
    for (int i = 0; i < n; i++)
    {
        int temp;
        cin >> temp;
        oArray.PushBack(temp);
    }
    //插入第二個陣列的資料
    for (int i = 0; i < n; i++)
    {
        int temp;
        cin >> temp;
        oArray2.PushBack(temp);
    }
    oArray.Print();//列印陣列
    
    oArray.Merge(oArray2);//合併兩個陣列
    oArray.Print();//重新列印
    return 0;
}
輸入資料:
6
9 4 6 11 5 8
7 10 6 18 2 4

輸出資料:
print all date :
4
5
6
8
9
11

合併陣列後的輸出資料:
print all date :
2
4
4
5
6
6
7
8
9
10
11
18

這樣就實現了一個有序陣列就,這個地方和前面一樣也只是int作為陣列的型別,並沒有使用模板,所以還可以擴充套件開來,實現更多的功能。最後給出所有檔案的程式碼。

所有檔案原始碼

COrderArray.h檔案的全部程式碼如下:

#pragma once
#include "CArray.h"
class COrderArray ://依然是繼承陣列基類
    public CArray
{
public:
    COrderArray(int size);//建構函式
    void PushBack(int value);//插入資料
    void RemoveForValue(int value);//根據指定值刪除陣列元素
    void RemoveForPos(int pos);//根據指定位置刪除陣列元素
    int FindForValue(int value);//根據指定值查詢陣列元素
    int &operator[](int pos);
    //過載下標運算子,達到按位置運算元組元素的目的
    //過載下標運算子的時候,這裡的返回值必須是引用型別
    void Clear();//清空所有元素
    void Merge(COrderArray& array);//合併兩個有序陣列
private:
    void _InsertElement(int value, int start);
    //因為在陣列中插入值的時候會將該位置後的所有的元素都向後移一個位置,引數為插入值和後移的初始位
    //所以單獨寫一個私有函式來完成插入並後移的功能,防止函式體內容太多,該函式也是不能在外部去呼叫
    void _RemoveElement(int pos);
    //和上面同理,這個函式是用來刪除陣列中某個位置的元素,刪除完成後把該位置後面的元素前移
    
    //注意:這兩個函式和上面的的插入、刪除函式的不同之處在於上面兩個公有函式負責給使用者呼叫
    //並且實現用什麼樣的策略去刪除或者插入,它們是內部呼叫下面的兩個私有函式
    //而這兩個私有函式只是是完成插入和刪除的功能
};

COrderArray.cpp檔案的全部程式碼如下:

#include<iostream>
#include "COrderArray.h"
COrderArray::COrderArray(int size)
{
    m_Capacity = size;
    mp_Array = new int[m_Capacity];
}

//在陣列中要插入的元素位置之後的所有元素進行後移
void COrderArray::_InsertElement(int value, int pos)
{
    for (int i = m_Size; i > pos; i--)
    {
        mp_Array[i] = mp_Array[i - 1];
    }
    mp_Array[pos] = value;
}

//插入元素,判斷元素大小來決定其應該在陣列中位置
void COrderArray::PushBack(int value)
{
    if (m_Size == m_Capacity)
    {
        return;
    }

    //二分查詢
    int start = 0;
    int end = m_Size;
    int mid = (start + end) / 2;
    while (1)
    {
        //陣列還沒有元素的時候,直接插入
        if (m_Size == 0)
        {
            mp_Array[m_Size] = value;
            break;
        }
        //迴圈跳出的條件,即mid與首尾之一相等
        if (mid==start||mid==end)
        {
            if (mp_Array[mid] > value)
            {
                _InsertElement(value, mid);
                break;
            }
            _InsertElement(value, mid + 1);
            break;
        }
        //大小判別,然後改變mid的值進行二分
        if (mp_Array[mid] > value)
        {
            end = mid;
            mid = (start + end) / 2;
        }
        else
        {
            start = mid;
            mid = (start + end) / 2;
        }
    }
    m_Size++;
}

//根據值查詢,二分查詢,和上面的插入類似
int COrderArray::FindForValue (int value)
{
    int start = 0;
    int end = m_Size;
    int mid = (start + end) / 2;
    while (mid != start && mid != end)
    {
        if (mp_Array[mid] == value)
        {
            return mid;
        }
        if (mp_Array[mid] > value)
        {
            end = mid;
            mid = (start + end) / 2;
        }
        else
        {
            start = mid;
            mid = (start + end) / 2;
        }
    }
    if (mp_Array[mid] == value)
    {
        return mid;
    }
}

//刪除操作,把刪除的數後面每一個元素向前移動一個位置
void COrderArray::_RemoveElement(int pos)
{
    if (pos >= 0 && pos < m_Size)
    {
        for (int i = pos; i < m_Size; i++)
        {
            mp_Array[i] = mp_Array[i + 1];
        }
        m_Size--;
    }
}

//根據指定位置刪除元素
void COrderArray::RemoveForPos(int pos)
{
    _RemoveElement(pos);
}

//根據指定值刪除元素
void COrderArray::RemoveForValue(int value)
{
    int pos = FindForValue(value);
    _RemoveElement(pos);
}
//過載下標運算子
int &COrderArray::operator[](int pos)
{
    if (pos > m_Size || pos < 0)
    {
        return mp_Array[0];
    }
    return mp_Array[pos];
}
//清空所有元素
void COrderArray::Clear()
{
    m_Size = 0;
}
//合併兩個有序陣列
void COrderArray::Merge(COrderArray& tempArray)
{
    //開一個新的記憶體來儲存合併後的陣列,首先需要計算新的記憶體需要多大
    int tempArrayCapacity = tempArray.getCapacity();
    int newCapacity = tempArrayCapacity + m_Capacity;
    int* newSpace = new int[newCapacity];

    //從兩個陣列當前size的位置開始合併
    int tempArraySize = tempArray.getSize();
    int tempSize = tempArraySize + m_Size;
    int pos = tempSize - 1;
    while (pos >= 0)
    {
        if (m_Size != 0)
        {
            if (mp_Array[m_Size - 1] >= tempArray[tempArraySize - 1]|| tempArraySize==0)
            {
                newSpace[pos] = mp_Array[m_Size - 1];
                m_Size--;
            }
            else
            {
                newSpace[pos] = tempArray[tempArraySize - 1];
                tempArraySize--;
            }
        }else
        {
            newSpace[pos] = tempArray[tempArraySize - 1];
            tempArraySize--;
        }
        pos--;
    }
    delete[] mp_Array;
    m_Size = tempSize;
    m_Capacity = newCapacity;
    mp_Array = newSpace;
}

Array.cpp檔案的全部程式碼如下:

#include<iostream>
using namespace std;
#include "COrderArray.h"
using namespace std;
int main()
{
    int n;
    cin >> n;

    //COrderArray* oArray = new COrderArray(n);
    COrderArray oArray(n);
    COrderArray oArray2(n);
    //插入第一個陣列的資料
    for (int i = 0; i < n; i++)
    {
        int temp;
        cin >> temp;
        oArray.PushBack(temp);
    }
    //插入第二個陣列的資料
    for (int i = 0; i < n; i++)
    {
        int temp;
        cin >> temp;
        oArray2.PushBack(temp);
    }
    oArray.Print();//列印陣列
    
    oArray.Merge(oArray2);//合併兩個陣列
    oArray.Print();//重新列印
    return 0;
}