C++: Simulationsimplementierung zum Hinzufügen, Löschen, Überprüfen und Ändern von Listen

Vorwort

Dieser Blog verwendet die SGI-Version. Da es sich bei den meisten Blogs um Anfänger handelt, liefert der Blogger nur nützliche Ausschnitte.C++: Die Simulationsimplementierung des Hinzufügens, Löschens, Überprüfens und Änderns von Listen besteht darin, C++ zum Kopieren der doppelt verknüpften Liste zu verwenden, was sehr einfach ist. Die Schwierigkeit liegt hauptsächlich in der Implementierung von Iteratoren

1. Listen Sie die zugrunde liegende doppelte verknüpfte Listenüberprüfung und Knotenkonstruktion auf

1.1 Liste der zugrunde liegenden Datenstruktur

Welche Datenstruktur wird am Ende der Liste zur Implementierung verwendet? Werfen wir zunächst einen Blick auf die Mitgliedsfunktionen und die Initialisierung der Liste in der SGI-Bibliothek.
Fügen Sie hier eine Bildbeschreibung ein

Wir haben festgestellt, dass es in der Listenimplementierung nur einen Mitgliedsvariablenknoten gibt. Der Konstruktor erstellt ein Ende an Ende verbundenes Sentinel-Bit. Gleichzeitig wird überprüft, ob die unterste Ebene der Liste eine doppelt verknüpfte Liste mit Sentinel-Bits ist.

1. 2 Knotenstruktur

Knoten haben die gleichen drei Mitglieder wie doppelt verknüpfte Listen: Knotendaten, die auf den vorherigen Knoten zeigen (prev) und auf den nächsten Knoten zeigen (next).

//节点
template<class T>
struct List_node
{
    
    
	T _data;
	List_node<T>* _prev;
	List_node<T>* _next;

	List_node(const T& x = T())
		:_data(x)
		,_prev(nullptr)
		,_next(nullptr)
	{
    
    }
};

Kleine Tipps:

  1. Unser Klassenname ist hier derselbe wie in der Bibliothek (List_node) und wird typdefiniert, wenn er später in anderen Klassen verwendet wird.
  2. Der Grund dafür, dass der Klassenname hier „struct“ und nicht „class“ lautet, liegt darin, dass die Standardzugriffsberechtigung von struct öffentlich ist und nachfolgende andere Klassen sie nur in großem Umfang verwenden müssen. Wenn Sie eine Klasse verwenden, müssen Sie eine große Anzahl von Freundklassen verwenden, was zu umständlich ist.

2. Implementierung der Iteratorkapselung (wichtige Punkte und Schwierigkeiten)

2.1 Vorläufige Hinweise

  1. Wir wissen, dass der größte Nutzen von Iteratoren das Durchlaufen von Daten ist. Aber wenn es stoppt, was passiert dann, wenn der Iterator auf den Speicherplatz zum Speichern von Daten zeigt? Das führt dazu, dass wir !=, ++, * und andere Operationen verwenden müssen. Benutzerdefinierte Typen können die Verwendung dieser Operatoren jedoch nicht unterstützen. Die hierfür angegebene Lösung lautet:Kapselung plus Operatorüberlastung
  2. Iteratoren werden in gewöhnliche Iteratoren und konstante Iteratoren unterteilt (auch unterteilt in Einweg-Iteratoren, Zwei-Wege-Iteratoren und Iteratoren mit wahlfreiem Zugriff). Eine der einfachsten Implementierungen besteht darin, zwei Klassen zu implementieren. Aber. . . Wir wissen, dass der Unterschied zwischen den beiden Iteratoren darin besteht, dass der Const-Iterator die Daten nicht ändern kann. Es ist nur so, dass die j-bezogenen Ausreden (hier sind *, ->) unterschiedlich sind. Es wäre zu „großer Aufwand“, zwei zu implementieren Klassen.
    Schauen wir uns also an, wie die Bibliothek dieses Problem clever löst!
    Fügen Sie hier eine Bildbeschreibung ein

2.2 Iterator-Implementierung

In der Vorerklärung wird auch erläutert, wie eine Klasse implementiert wird, um den Zweck zu erreichen. Die folgende Implementierung ist zu einfach und wird nicht separat erläutert.

//迭代器封装
template<class T, class Ref, class Ptr>
struct __list_iterator
{
    
    
	typedef List_node<T> Node;//节点类名重命名
	//由于迭代器实现中,如++、--等重载函数返回值类型为__list_iterator,名字过长,这里我们重命名self意味自身
	typedef __list_iterator<T,Ref, Ptr> self;
	Node* _node;//成员变量

	__list_iterator(Node* node)//构造出一个节点
		:_node(node)
	{
    
    }

	//前置++
	self& operator++()
	{
    
    
		_node = _node->_next;
		return *this;
	}
	//前置--
	self& operator--()
	{
    
    
		_node = _node->_prev;
		return *this;
	}
	
	//后置++
	self operator++(int)
	{
    
    
		self tmp(*this);
		_node = _node->_next;
		return tmp;
	}
	//后置--
	self operator--(int)
	{
    
    
		self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}
	
	//解引用操作符重载
	Ref operator*()
	{
    
    
		return _node->_data;
	}
	
	//用于访问迭代器指向对象中存储的是自定义类型
	Ptr operator->()
	{
    
    
		return &_node->_data;
	}

	bool operator!=(const self& s)
	{
    
    
		return _node != s._node;
	}

	bool operator==(const self& s)
	{
    
    
		return _node == s._node;
	}
};

3. Listenimplementierung

3.1 Grundgerüst

In der Listensimulation geben wir einem Kopfknoten _head und _size zwei Mitgliedsvariablen, genau wie in der Bibliothek. Gleichzeitig benennen wir die Knoten und Iteratoren um. Das Umbenennen von Iteratoren dient nicht nur der Bequemlichkeit, sondern, was noch wichtiger ist, es bietet eine einheitliche Schnittstelle (z. B. Sting-, Vektor-, Set-Schnittstellen usw. sind alle gleich), schützt die zugrunde liegenden Variablen und Mitgliedsfunktionsattribute sowie den Implementierungsprozess und Unterschiede.

//list模拟实现
template<class T>
class List
{
    
    
	typedef List_node<T> Node;
public:
	//迭代器重命名,提供统一的接口,屏蔽底层实现细节和差异
	typedef __list_iterator<T, T&, T*> iterator;
	typedef __list_iterator<T, const T&, const T*> const_iterator;
private:
	Node* _head;//头节点
	int _size;
};

3.2 Iteratoren und Const-Iteratoren

Unten sehen Sie, wo begin() und end() auf eine gültige zweizeilige Tabelle verweisen.
Fügen Sie hier eine Bildbeschreibung ein

erreichen:

const_iterator begin()const
{
    
    
	//return const_iterator(_head->_next);或
	return _head->_next;//单参数类型支持隐式类型转换
}

const_iterator end()const
{
    
    
	return _head;
}

iterator begin()
{
    
    
	return _head->_next;
}

iterator end()
{
    
    
	return _head;
}

3.2 Konstruktor, Destruktor, Kopierkonstruktion, Zuweisungsüberladung

3.2.1 Konstruktor

Die Implementierung des Konstruktors ist die gleiche wie das, was wir am Anfang gesehen haben: Eine Funktion (empty_Init) wird im Konstruktor aufgerufen, um sie zu implementieren. empty_Init() wird verwendet, um die Initialisierung abzuschließen. (Die Funktion empty_Init() wird später auch von anderen Schnittstellen aufgerufen)

//初始化
void empty_Init()
{
    
    
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;

	_size = 0;
}

//无参构造
List()
{
    
    
	empty_Init();
}

3.2.2 Destruktor

Im Destruktor rufen wir eine Clear-Funktion auf, um alle Daten zu löschen, und geben dann die Variable _head frei.

//析构函数
~List()
{
    
    
	clear();
	delete _head;
	_head = nullptr;
}

void clear()
{
    
    
	iterator it = begin();
	while (it != end())
	{
    
    
		it = erase(it);
	}
}

3.2.3 Konstruktion kopieren

Beim Kopieren und Erstellen müssen Sie zuerst einen Knoten initialisieren und dann die zu kopierenden Daten per Tail-Insert einfügen (die Implementierung der Tail-Insert-Schnittstelle wird später angegeben).

//拷贝构造
List(const List<T>& It)
{
    
    
	empty_Init();
	for (auto e : It)
	{
    
    
		push_back(e);
	}
}

3.2.4 Zuweisungsüberladung

Es gibt viele Möglichkeiten, die Zuweisung zu überlasten. Bei relativ einfachen Parametern übergeben wir den Wert direkt und tauschen dann die Daten der zuzuweisenden Variablen mit der temporären Variablen aus, die durch Übergabe des Parameters als Wert generiert wird.

void swap(const List<T>& It)
{
    
    
	std::swap(_head, It._head);
}

//赋值重载
List<T>& operator=(const List<T> It)
{
    
    
	swap(It);
	return *this;
}

3.3 An beliebiger Position einfügen, an beliebiger Position löschen, Einfügen beenden, Löschen beenden, Kopf einfügen, Kopf löschen

3.3.1 An beliebiger Position einfügen

Zuerst nimmt new den einzufügenden Knoten newnode heraus und verbindet dann prev, newnode und pos, den Knoten vor pos. Schließlich reicht ++_size aus.
Fügen Sie hier eine Bildbeschreibung ein

iterator insert(iterator pos, const T& x)
{
    
    
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(x);

	prev->_next = newnode;
	newnode->_prev = prev;

	newnode->_next = cur;
	cur->_prev = newnode;
	_size++;
	return newnode;
}

3.3.2 Einfügen und Löschen an beliebiger Stelle

Speichern Sie zuerst die vorderen und hinteren Knoten der zu löschenden Daten, löschen Sie dann den Knoten an der Position und verbinden Sie dann die vorderen und hinteren Knoten. Schließlich reicht –_size.

iterator erase(iterator pos)
{
    
    
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;
	delete cur;
	prev->_next = next;
	next->_prev = prev;

	_size--;
	return next;
}

3.3.3 Schwanzeinfügung, Schwanzlöschung, Kopfeinfügung, Kopflöschung

Schwanzeinfügung, Schwanzlöschung, Kopfeinfügung und Kopflöschung verwenden alle die Einfüge- und Löschschnittstellen an jeder Position wieder.

void push_back(const T& x)
{
    
    
	insert(end(), x);
}

void push_front(const T& x)
{
    
    
	insert(begin(), x);
}

void pop_back()
{
    
    
	erase(--end());
}

void pop_front()
{
    
    
	erase(begin());
}

4. Die Listenfunktion ist perfekt

4.1 Iteratoroperator->()

Schauen wir uns zunächst den folgenden Code an, oder?

struct AA
{
    
    
	AA(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{
    
    }

	int _a1;
	int _a2;
};

void test_list3()
{
    
    
	list<AA> lt;
	lt.push_back(AA(1, 1));
	lt.push_back(AA(2, 2));
	lt.push_back(AA(3, 3));

	list<AA>::iterator it = lt.begin();
	while (it != lt.end())
	{
    
    
		cout<<*it<<" ";
		++it;
	}
	cout << endl;
}

Da die Liste << nicht überlädt, wird beim Zugriff auf gespeicherte benutzerdefinierte Typen ein Kompilierungsfehler gemeldet.
Können wir dann nicht einfach den <<-Operator überladen? Leider unterstützt die C++-Bibliothek << in der Liste nicht. Der Hauptgrund dafür ist, dass der Compiler nicht weiß, wie er die Daten erhält.

Bei benutzerdefinierten Typen können wir also zuerst dereferenzieren und dann auf die Mitglieder zugreifen, oder wir können die Funktion „operator->()“ im Iterator überladen. Es ist jedoch zu beachten, dass der Compiler ein -> verbirgt. Das spezifische native Verhalten ist wie folgt:

struct AA
{
    
    
	AA(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{
    
    }

	int _a1;
	int _a2;
};

void test_list3()
{
    
    
	list<AA> lt;
	lt.push_back(AA(1, 1));
	lt.push_back(AA(2, 2));
	lt.push_back(AA(3, 3));

	list<AA>::iterator it = lt.begin();
	while (it != lt.end())
	{
    
    
		//cout << (*it)._a1<<" "<<(*it)._a2 << endl;
		cout << it->_a1 << " " << it->_a2 << endl;
		//上面编译器访问成员变量的真正行为如下:
		//cout << it.operator->()->_a1 << " " << it.operator->()->_a1 << endl;
		++it;
	}
	cout << endl;
}

4.2 Daten drucken

//大多数情况模板是class还是typename是一样的,但当有未实例化模板时,必须使用typename
//template<typename T>
void print_list(const list<T>& lt)
{
    
    
	// list<T>未实例化的类模板,编译器不能去他里面去找
	// 编译器就无法list<T>::const_iterator是内嵌类型,还是静态成员变量
	// 前面加一个typename就是告诉编译器,这里是一个类型,等list<T>实例化
	// 再去类里面去取
	typename list<T>::const_iterator it = lt.begin();
	while (it != lt.end())
	{
    
    
		cout << *it << " ";
		++it;
	}
	cout << endl;  
}

Optimierung: Die oben gedruckten Daten gelten für die Liste und die folgenden für den Container.

// 模板(泛型编程)本质,本来应该由我们干的活交给编译器
template<typename Container>
void print_container(const Container& con)
{
    
    
	typename Container::const_iterator it = con.begin();
	while (it != con.end())
	{
    
    
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

5. Alle Codes und Testfälle

giteeC++: Simulationsimplementierungscode zum Hinzufügen, Löschen, Überprüfen und Ändern sowie Testfallempfehlung auflisten
: giteeC++: Simulationsimplementierungscode zum Hinzufügen, Löschen, Überprüfen und Ändern auflisten (endgültige Version, Gefühlsversion!!!)

Supongo que te gusta

Origin blog.csdn.net/Zhenyu_Coder/article/details/135185031
Recomendado
Clasificación