이진트리
이진트리는 각 노드마다 최대 두개의 자식 노드를 가진다. 루트부터 리프까지의 최대 거리와 최소 거리의 차이가 1 이하이면 균형이 잡혔다고 표현한다. 균형을 잡는 작업은 다소 까다롭고 속도도 느리기 때문에, 노드의 수가 많을수록 균형을 잡은 상태로 만들기 시작하는 것이 좋다.
package main
import (
"fmt"
"math/rand"
"time"
)
type Tree struct {
Left *Tree
Value int
Right *Tree
}
func traverse(t *Tree) {
if t == nil {
return
}
traverse(t.Left)
fmt.Print(t.Value, " ")
traverse(t.Right)
}
func create(n int) *Tree {
var t *Tree
rand.Seed(time.Now().Unix())
for i := 0; i < 2*n; i++ {
temp := rand.Intn(n * 2)
t = insert(t, temp)
}
return t
}
func insert(t *Tree, v int) *Tree {
if t == nil {
return &Tree{nil, v, nil}
}
if v == t.Value {
return t
}
if v < t.Value {
t.Left = insert(t.Left, v)
return t
}
t.Right = insert(t.Right, v)
return t
}
func main() {
tree := create(10)
fmt.Println("Root : ", tree.Value)
traverse(tree)
fmt.Println()
tree = insert(tree, -10)
tree = insert(tree, -2)
traverse(tree)
fmt.Println()
fmt.Println("Root : ", tree.Value)
}
- traverse() 함수는 이진 트리를 구성하는 모든 노드를 재귀 호출한다.
- create() 함수는 이진 트리를 정수형 난수로 채운다.
- insert() 함수는 이진 트리의 속성 대로 숫자를 트리에 삽입한다.
$ go run foo.go
Root : 10
1 4 9 10 11 12 13 14 15 18 19
-10 -2 1 4 9 10 11 12 13 14 15 18 19
Root : 10
이진 트리의 장점
계층형 데이터를 표현하는데 트리만큼 좋은 것은 업다. 프로그래밍 언어의 컴파일러에서 프로그램을 파싱할 때 트리를 굉장히 많이 사용한다.
트리는 기본적으로 순서를 가진다. 다시 말해 정렬하기 위한 작업을 따로 수행할 필요가 없다. 원소를 적절한 위치에 추가하는 과정이 곧 정렬하는 과정이다. 하지만 원소를 삭제하는 것은 트리의 구성 방식에 따라 쉽지 않을 수 있다.
이진 트리의 균형이 잡혀 있다면 모든 연산은 대략 log n이다. 또한 균형 이진 트리의 높이는 대략 log2n이다.
이진 트리의 큰 단점은 트리의 모양이 원소를 추가하는 순서에 따라 달라진다는 것이다.
이진 트리보다 연결 리스트나 배열이 훨씬 빠르지만, 검색 연산에 대한 유연성은 이러한 성능 및 관리의 오버헤드를 상쇄하고도 남는다. 이진 트리에서 특정한 원소를 탐색할 때 그 원소의 값이 현재 노드보다 작은지 검사할 수 있고, 이러한 방법으로 트리를 탐색할 때 방향을 결정할 수 있다.
해시 테이블 만들기
해시 테이블은 한 개 이상의 키와 값을 쌍을 저장하는 자료 구조이다. 해시 함수는 버킷 또는 슬롯 배열에서 원하는 값이 담긴 위치를 계산할 때 사용한다. 이상적인 해시 함수는 각 키마다 고유한 버킷을 가리켜야한다. 단 버킷의 수는 필요한 만큼 있어야한다.
좋은 해시 함수는 고르게 분산된 해시값을 생성한다. 사용하지 않는 버킷이 많거나 버킷 사이의 크기가 심하면 성능이 떨어지기 때문이다.
package main
import "fmt"
const SIZE = 15
type Node struct {
Value int
Next *Node
}
type HashTable struct {
Table map[int]*Node
Size int
}
func hashFunction(i, size int) int {
return (i % size)
}
func insert(hash *HashTable, value int) int {
index := hashFunction(value, hash.Size)
element := Node{Value : value, Next : hash.Table[index]}
hash.Table[index] = &element
return index
}
func traverse(hash *HashTable) {
for k := range hash.Table {
if hash.Table[k] != nil {
t := hash.Table[k]
for t != nil {
fmt.Printf("%d -> ", t.Value)
t = t.Next
}
fmt.Println()
}
}
}
func lookup(hash *HashTable, value int) bool {
index := hashFunction(value, hash.Size)
if hash.Table[index] != nil {
t := hash.Table[index]
if t != nil {
if t.Value == value {
return true
}
t = t.Next
}
}
return false
}
func main() {
table := make(map[int]*Node, SIZE)
hash := &HashTable{Table: table, Size: SIZE}
fmt.Println("Num of spaces :", hash.Size)
for i := 0; i < 120; i++ {
insert(hash, i)
}
traverse(hash)
}
해시 테이블에 n개의 키와 k개의 버킷이 있다면 n개의 키에 대한 탐색 속도를 O n 에서 O n/k로 높일 수 있다.
$ go run foo.go
Num of spaces : 15
106 -> 91 -> 76 -> 61 -> 46 -> 31 -> 16 -> 1 ->
109 -> 94 -> 79 -> 64 -> 49 -> 34 -> 19 -> 4 ->
113 -> 98 -> 83 -> 68 -> 53 -> 38 -> 23 -> 8 ->
116 -> 101 -> 86 -> 71 -> 56 -> 41 -> 26 -> 11 ->
105 -> 90 -> 75 -> 60 -> 45 -> 30 -> 15 -> 0 ->
112 -> 97 -> 82 -> 67 -> 52 -> 37 -> 22 -> 7 ->
115 -> 100 -> 85 -> 70 -> 55 -> 40 -> 25 -> 10 ->
117 -> 102 -> 87 -> 72 -> 57 -> 42 -> 27 -> 12 ->
119 -> 104 -> 89 -> 74 -> 59 -> 44 -> 29 -> 14 ->
108 -> 93 -> 78 -> 63 -> 48 -> 33 -> 18 -> 3 ->
114 -> 99 -> 84 -> 69 -> 54 -> 39 -> 24 -> 9 ->
118 -> 103 -> 88 -> 73 -> 58 -> 43 -> 28 -> 13 ->
107 -> 92 -> 77 -> 62 -> 47 -> 32 -> 17 -> 2 ->
110 -> 95 -> 80 -> 65 -> 50 -> 35 -> 20 -> 5 ->
111 -> 96 -> 81 -> 66 -> 51 -> 36 -> 21 -> 6 ->
연결 리스트 만들기
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
var root = new(Node)
func addNode(t *Node, v int) int {
if root == nil {
t = &Node{v, nil}
root = t
return 0
}
if v == t.Value {
fmt.Println("Node already exists : ", v)
return -1
}
if t.Next == nil {
t.Next = &Node{v, nil}
return -2
}
return addNode(t.Next, v)
}
func traverse(t *Node) {
if t == nil {
fmt.Println("-> Empty list!")
return
}
for t != nil {
fmt.Printf("%d -> " , t.Value)
t = t.Next
}
fmt.Println()
}
func lookupNode(t *Node, v int) bool {
if root == nil {
t = &Node{v, nil}
root = t
return false
}
if v == t.Value {
return true
}
if t.Next == nil {
return false
}
return lookupNode(t.Next, v)
}
func size(t *Node) int {
if t == nil {
fmt.Println("-> Empty list!")
return 0
}
i := 0
for t != nil {
i++
t = t.Next
}
return i
}
func main() {
fmt.Println(root)
root = nil
traverse(root)
addNode(root, 1)
addNode(root, -1)
traverse(root)
addNode(root, 10)
addNode(root, 5)
addNode(root, 45)
addNode(root, 5)
addNode(root, 5)
traverse(root)
addNode(root, 100)
traverse(root)
if lookupNode(root, 100) {
fmt.Println("Node exists!")
} else {
fmt.Println("Node does not exists!")
}
if lookupNode(root, -100) {
fmt.Println("Node exists")
} else {
fmt.Println("Node does not exists!")
}
}
$ go run foo.go
&{0 <nil>}
-> Empty list!
1 -> -1 ->
Node already exists : 5
Node already exists : 5
1 -> -1 -> 10 -> 5 -> 45 ->
1 -> -1 -> 10 -> 5 -> 45 -> 100 ->
Node exists!
Node does not exists!
정렬된 연결 리스트를 사용하면 노드를 탐색하거나 추가하는 과정에서 다양한 최적화 기법을 적용할 수 있다. 가장 흔히 사용하는 방법은 정렬된 연결 리스트의 중심 노드에 포인터를 저장하고 있다가 탐색을 수행할 때 그 지점에서 시작한다.
이중 연결 리스트 만들기
package main
import "fmt"
type Node struct {
Value int
Previous *Node
Next *Node
}
var root = new(Node)
func addNode(t *Node, v int) int {
if root == nil {
t = &Node{v, nil, nil}
root = t
return 0
}
if v == t.Value {
fmt.Println("Node already exists")
return -1
}
if t.Next == nil {
temp := t
t.Next = &Node{v, temp, nil}
return -2
}
return addNode(t.Next, v)
}
func traverse(t *Node) {
if t == nil {
fmt.Println("-> Empty list")
return
}
for t != nil {
fmt.Println("-> Empty list")
return
}
fmt.Println()
}
func reverse(t *Node) {
if t == nil {
fmt.Println("-> Empty list!")
return
}
temp := t
for t != nil {
temp = t
t = t.Next
}
for temp.Previous != nil {
fmt.Printf("%d -> ", temp.Value)
temp = temp.Previous
}
fmt.Printf("%d -> ", temp.Value)
fmt.Println()
}
func size(t *Node) int {
if t == nil {
fmt.Println("-> Empty list!")
return 0
}
n := 0
for t != nil {
n++
t = t.Next
}
return n
}
func lookupNode(t *Node, v int) bool {
if root == nil {
return false
}
if v == t.Value {
return true
}
if t.Next == nil {
return false
}
return lookupNode(t.Next, v)
}
func main() {
fmt.Println(root)
root = nil
traverse(root)
addNode(root, 1)
addNode(root, -1)
traverse(root)
addNode(root, 10)
addNode(root, 5)
addNode(root, 0)
addNode(root, 0)
traverse(root)
addNode(root, 100)
fmt.Println("Size :", size(root))
traverse(root)
reverse(root)
}
$ go run foo.go
&{0 <nil> <nil>}
-> Empty list
-> Empty list
Node already exists
-> Empty list
Size : 6
-> Empty list
100 -> 0 -> 5 -> 10 -> -1 -> 1 ->
리스트를 탐색할 때 양방향 모두 이동할 수 있고 원소를 더 쉽게 추가하거나 삭제할 수 있다. 헤드에 대한 포이터를 잃어버리더라도 리스트의 헤드 노드를 찾아 낼 수 있다.
큐 만들기
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
var size = 0
var queue = new(Node)
func Push(t *Node, v int) bool {
if queue == nil {
queue = &Node{v, nil}
size++
return true
}
t = &Node{v, nil}
t.Next = queue
queue = t
size++
return true
}
func Pop(t *Node) (int, bool) {
if size == 0 {
return 0, false
}
if size == 1 {
queue = nil
size--
return t.Value, true
}
temp := t
for t.Next != nil {
temp = t
t = t.Next
}
v := temp.Next.Value
temp.Next = nil
size--
return v, true
}
func traverse(t *Node) {
if size == 0 {
fmt.Println("Empty Q")
return
}
for t != nil {
fmt.Printf("%d -> ", t.Value)
t = t.Next
}
fmt.Println()
}
func main() {
queue = nil
Push(queue, 10)
fmt.Println("Size:", size)
traverse(queue)
v, b := Pop(queue)
if b {
fmt.Println("Pop:", v)
}
fmt.Println("Size : ", size)
for i := 0; i < 5; i++ {
Push(queue, i)
}
traverse(queue)
fmt.Println("Size :", size)
v, b = Pop(queue)
if b {
fmt.Println("Pop : ", v)
}
fmt.Println("Size :", size)
v, b = Pop(queue)
if b {
fmt.Println("Pop : ", v)
}
fmt.Println("Size :", size)
traverse(queue)
}
$ go run foo.go
Size: 1
10 ->
Pop: 10
Size : 0
4 -> 3 -> 2 -> 1 -> 0 ->
Size : 5
Pop : 0
Size : 4
Pop : 1
Size : 3
4 -> 3 -> 2 ->
스택 만들기
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
var size = 0
var stack = new(Node)
func Push(v int) bool {
if stack == nil {
stack = &Node{v, nil}
size = 1
return true
}
temp := &Node{v, nil}
temp.Next = stack
stack = temp
size++
return true
}
func Pop(t *Node) (int, bool) {
if size == 0 {
return 0, false
}
if size == 1 {
size = 0
stack = nil
return t.Value, true
}
stack = stack.Next
size--
return t.Value, true
}
func traverse(t *Node) {
if size == 0 {
fmt.Println("Empty stack")
return
}
for t != nil {
fmt.Printf("%d -> ", t.Value)
t = t.Next
}
fmt.Println()
}
func main() {
stack = nil
v, b := Pop(stack)
if b {
fmt.Print(v, " ")
} else {
fmt.Println("Pop() failed!")
}
Push(100)
traverse(stack)
Push(200)
traverse(stack)
for i := 0; i < 10; i++ {
Push(i)
}
for i := 0; i < 15; i++ {
v, b := Pop(stack)
if b {
fmt.Print(v, " ")
} else {
break
}
}
fmt.Println()
traverse(stack)
}
$ go run foo.go
Pop() failed!
100 ->
200 -> 100 ->
9 8 7 6 5 4 3 2 1 0 200 100
Empty stack
Container 패키지
컨테이너 페키지는 세 가지 자료 구조를 제공한다. (heap, list, ring)
링은 순환 리스트이며, 링의 마지막 원소는 첫번째 원소를 가리킨다.
Container/heap
힙은 일종의 트리로, 각 노드의 값은 그 노드의 아래에 있는 원소들 중에서 가장 작은 값으로 구성한다. 여기서 주목할 부분은, 최소값이라 표현하지 않고 가장 작은 값이라 표현한 점이다. 힙은 숫자가 아닌 다른 종류의 값도 표현한다는 것을 강조하기 위해서다.
container/heap 패키지를 사용하려면 container/heap.Interface를 구현해야한다.
type Interface interface {
sort.Interface
Push(x interface{})
Pop() interface{}
}
package main
import (
"container/heap"
"fmt"
)
type heapFloat32 []float32
func (n *heapFloat32) Pop() interface{} {
old := *n
x := old[len(old) - 1]
new := old[0 : len(old) - 1]
*n = new
return x
}
func (n *heapFloat32) Push(x interface{}) {
*n = append(*n, x.(float32))
}
func (n heapFloat32) Len() int {
return len(n)
}
func (n heapFloat32) Less(a,b int) bool {
return n[a] < n[b]
}
func (n heapFloat32) Swap(a, b int) {
n[a], n[b] = n[b], n[a]
}
func main() {
myHeap := &heapFloat32{1.2, 2.1, 3.1, -100.1}
heap.Init(myHeap)
size := len(*myHeap)
fmt.Printf("Heap Size : %d\n", size)
fmt.Printf("%v\n", myHeap)
myHeap.Push(float32(-100.2))
myHeap.Push(float32(0.2))
fmt.Printf("Heap Size : %d\n", size)
fmt.Printf("%v\n", myHeap)
heap.Init(myHeap)
fmt.Printf("%v\n", myHeap)
}
$ go run foo.go
Heap Size : 4
&[-100.1 1.2 3.1 2.1]
Heap Size : 4
&[-100.1 1.2 3.1 2.1 -100.2 0.2]
&[-100.2 -100.1 0.2 2.1 1.2 3.1]
container/list 사용법
package main
import (
"container/list"
"fmt"
"strconv"
)
func printList(l *list.List) {
for t := l.Back(); t != nil; t = t.Prev() {
fmt.Print(t.Value, " ")
}
fmt.Println()
for t := l.Front(); t != nil; t = t.Next() {
fmt.Print(t.Value, " ")
}
fmt.Println()
}
func main() {
values := list.New()
e1 := values.PushBack("One")
e2 := values.PushBack("Two")
values.PushFront("Three")
values.InsertBefore("Four", e1)
values.InsertAfter("Five", e2)
values.Remove(e2)
values.Remove(e2)
values.InsertAfter("FiveFive", e2)
values.PushBackList(values)
printList(values)
values.Init()
fmt.Printf("After Init(): %v\n", values)
for i := 0; i < 20; i++ {
values.PushFront(strconv.Itoa(i))
}
printList(values)
}
$ go run foo.go
Five One Four Three Five One Four Three
Three Four One Five Three Four One Five
After Init(): &{{0xc000056180 0xc000056180 <nil> <nil>} 0}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
list.PushBack() 함수는 연결 리스트의 마지막에 오브젝트를 추가하는 반면 list.PushFront() 함수는 리스트의 첫 부분에 오브젝트를 추가한다. 두 함수는 리스트에 추가한 값을 리턴한다.
container/ring 사용법
package main
import (
"container/ring"
"fmt"
)
var size int = 10
func main() {
myRing := ring.New(size + 1)
fmt.Println("Empty ring :", *myRing)
for i := 0; i < myRing.Len()-1; i++ {
myRing.Value = i
myRing = myRing.Next()
}
myRing.Value = 2
sum := 0
myRing.Do(func(x interface{}) {
t := x.(int)
sum += t
})
fmt.Println("Sum:", sum)
for i := 0; i < myRing.Len()+2; i++ {
myRing = myRing.Next()
fmt.Print(myRing.Value, " ")
}
fmt.Println()
}
ring .Do 함수는 링의 각 원소에 대해 함수를 호출할 때 사용한다. 이 하수가 링을 수정하면 ring.Do()의 동작을 알 수 없게 된다. x.(int)라고 적은 부분을 타입 어써션이라고 부른다.
$ go run foo.go
Empty ring : {0xc0000a6040 0xc0000a6160 <nil>}
Sum: 47
0 1 2 3 4 5 6 7 8 9 2 0 1
난수 생성하기
난수를 생성하는 방법은 실용적일 뿐만 아니라 전산학의 한 연구 분야이다. 난수를 지정하려면 씨드를 지정해야 한다. 씨드란 전반적인 난수 생성 과정을 초기화하는데 사용되는 굉장히 중요한 값이다. 그 이유는 같은 씨드 값으로 시작하면 난수가 생성되는 결과도 같아지기 때문이다. 이렇게 누구나 같은 값들을 생성할 수 있다면 나수라 부를 수 없다.
package main
import (
"fmt"
"math/rand"
"os"
"strconv"
"time"
)
func random(min, max int) int {
return rand.Intn(max-min) + min
}
func main() {
MIN := 0
MAX := 100
TOTAL := 100
SEED := time.Now().Unix()
arguments := os.Args
switch len(arguments) {
case 2:
fmt.Println("Usage: ./randomNumbers MIN MAX TOTAL SEED")
MIN, _ = strconv.Atoi(arguments[1])
MAX = MIN + 100
case 3:
fmt.Println("Usage: ./randomNumbers MIN MAX TOTAL SEED")
MIN, _ = strconv.Atoi(arguments[1])
MAX, _ = strconv.Atoi(arguments[2])
case 4:
fmt.Println("Usage: ./randomNumbers MIN MAX TOTAL SEED")
MIN, _ = strconv.Atoi(arguments[1])
MAX, _ = strconv.Atoi(arguments[2])
TOTAL, _ = strconv.Atoi(arguments[3])
case 5:
MIN, _ = strconv.Atoi(arguments[1])
MAX, _ = strconv.Atoi(arguments[2])
TOTAL, _ = strconv.Atoi(arguments[3])
SEED, _ = strconv.ParseInt(arguments[4], 10, 64)
default:
fmt.Println("Using default values!")
}
rand.Seed(SEED)
for i := 0; i < TOTAL; i++ {
myrand := random(MIN, MAX)
fmt.Print(myrand)
fmt.Print(" ")
}
fmt.Println()
}
랜덤 스트링 생성하기
package main
import (
"fmt"
"math/rand"
"os"
"strconv"
"time"
)
func random(min, max int) int {
return rand.Intn(max-min) + min
}
func main() {
MIN := 0
MAX := 94
SEED := time.Now().Unix()
var LENGTH int64 = 8
arguments := os.Args
switch len(arguments) {
case 2:
LENGTH, _ = strconv.ParseInt(os.Args[1], 10, 64)
default:
fmt.Println("Using default values!")
}
rand.Seed(SEED)
startChar := "!"
var i int64 = 1
for {
myRand := random(MIN, MAX)
newChar := string(startChar[0] + byte(myRand))
fmt.Print(newChar)
if i == LENGTH {
break
}
i++
}
fmt.Println()
}
startChar 변수는 아스키 문자를 담는다. 이 값은 십진수로 33이다. 94 보다 작은 난수 생성시 가장 큰 아스키 값은 93+33인 126이다.
$ go run foo.go 20
cjFq$;)a:I4"57$0+G$^
'Go 언어 공부' 카테고리의 다른 글
[GO 마스터하기] 07-리플렉션과 인터페이스 (0) | 2020.09.21 |
---|---|
[GO 마스터하기] 06-Go Package (0) | 2020.09.21 |
[GO 마스터하기] 04-합성 타입 사용법 (0) | 2020.09.07 |
[GO 마스터하기] 03-Go언어의 기본 데이터 타입 (0) | 2020.09.07 |
[GO 마스터하기] 02-Go언어 내부 살펴보기 (0) | 2020.09.06 |